diff --git a/.gitmodules b/.gitmodules index af2dbee6..3e7fa1a8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "build-scripts"] path = build-scripts - url = https://github.com/TouchInstinct/BuildScripts.git + url = https://gitlab.ti/touchinstinct/BuildScripts.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 547b2094..4eb962b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # Changelog +### 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 diff --git a/LeadKit.podspec b/LeadKit.podspec index badd9a00..fa413634 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = "LeadKit" - s.version = "1.33.0" + s.version = "1.35.0" s.summary = "iOS framework with a bunch of tools for rapid development" - s.homepage = "https://github.com/TouchInstinct/LeadKit" + s.homepage = "https://gitlab.ti/touchinstinct/LeadKit" s.license = "Apache License, Version 2.0" s.author = "Touch Instinct" - s.source = { :git => "https://github.com/TouchInstinct/LeadKit.git", :tag => s.version } + s.source = { :git => "https://gitlab.ti/touchinstinct/LeadKit.git", :tag => s.version } s.platform = :ios, '10.0' s.swift_versions = ['5.1'] diff --git a/Package.resolved b/Package.resolved index 64252c9e..d91fdd25 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,79 +1,77 @@ { - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "f96b619bcb2383b43d898402283924b80e2c4bae", - "version": "5.4.3" - } - }, - { - "package": "Cache", - "repositoryURL": "https://github.com/hyperoslo/Cache.git", - "state": { - "branch": null, - "revision": "c7f4d633049c3bd649a353bad36f6c17e9df085f", - "version": "6.0.0" - } - }, - { - "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" - } + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "f96b619bcb2383b43d898402283924b80e2c4bae", + "version" : "5.4.3" } - ] - }, - "version": 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" : "a1561869135e72832eff3b1e729075c56c2eebf6", + "version" : "0.5.1" + } + }, + { + "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" : "9b906860e3c3c09032879465c471e6375829593f", + "version" : "15.0.0" + } + }, + { + "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" : "b4307ba0b6425c0ba4178e138799946c3da594f8", + "version" : "6.5.0" + } + }, + { + "identity" : "tablekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/maxsokolov/TableKit.git", + "state" : { + "revision" : "8bf4840d9d0475a92352f02f368f88b74eced447", + "version" : "2.11.0" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index b892c9e9..7c8693f0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.7 import PackageDescription let package = Package( @@ -11,17 +11,18 @@ let package = Package( // MARK: - UIKit .library(name: "TIUIKitCore", targets: ["TIUIKitCore"]), .library(name: "TIUIElements", targets: ["TIUIElements"]), + .library(name: "TIWebView", targets: ["TIWebView"]), // MARK: - SwiftUI .library(name: "TISwiftUICore", targets: ["TISwiftUICore"]), - + // MARK: - Utils .library(name: "TISwiftUtils", targets: ["TISwiftUtils"]), .library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]), .library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]), .library(name: "TITableKitUtils", targets: ["TITableKitUtils"]), - .library(name: "TILogging", targets: ["TILogging"]), .library(name: "TIDeeplink", targets: ["TIDeeplink"]), + .library(name: "TIDeveloperUtils", targets: ["TIDeveloperUtils"]), // MARK: - Networking @@ -39,7 +40,7 @@ let package = Package( .library(name: "TITransitions", targets: ["TITransitions"]), .library(name: "TIPagination", targets: ["TIPagination"]), .library(name: "TIAuth", targets: ["TIAuth"]), - + //MARK: - Skolkovo .library(name: "TIEcommerce", targets: ["TIEcommerce"]) ], @@ -52,21 +53,22 @@ let package = Package( .package(url: "https://github.com/hyperoslo/Cache.git", .upToNextMajor(from: "6.0.0")) ], targets: [ - + // MARK: - UIKit .target(name: "TIUIKitCore", dependencies: ["TISwiftUtils"], path: "TIUIKitCore/Sources"), .target(name: "TIUIElements", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIUIElements/Sources"), + .target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"), // MARK: - SwiftUI .target(name: "TISwiftUICore", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TISwiftUICore/Sources"), - + // MARK: - Utils .target(name: "TISwiftUtils", path: "TISwiftUtils/Sources"), - .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils"), + .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils", exclude: ["TIFoundationUtils.app"]), .target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"), .target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"), - .target(name: "TILogging", dependencies: ["TIUIElements", "TISwiftUtils", "TIUIKitCore"], path: "TILogging/Sources"), .target(name: "TIDeeplink", dependencies: ["TIFoundationUtils"], path: "TIDeeplink/Sources"), + .target(name: "TIDeveloperUtils", dependencies: ["TISwiftUtils", "TIUIKitCore", "TIUIElements"], path: "TIDeveloperUtils/Sources"), // MARK: - Networking .target(name: "TINetworking", dependencies: ["TIFoundationUtils", "Alamofire"], path: "TINetworking/Sources"), @@ -83,9 +85,9 @@ let package = Package( .target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"), .target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "KeychainAccess"], path: "TIAuth/Sources"), .target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"), - + // MARK: - Tests - + .testTarget( name: "TITimerTests", dependencies: ["TIFoundationUtils"], diff --git a/README.md b/README.md index e8ca5c99..78bbc14b 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,71 @@ This repository contains the following frameworks: - [TIYandexMapUtils](TIYandexMapUtils) - set of helpers for map objects clustering and interacting using Yandex Maps SDK. - [TIAuth](TIAuth) - login, registration, confirmation and other related actions -Useful docs: +## Playgrounds +### Create new Playground + +```sh +cd TIModuleName +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 +``` + +### Add new playground to pre release script + +`project-scripts/gen_docs_from_playgrounds.sh`: + +```sh +PLAYGROUNDS="${SRCROOT}/TIFoundationUtils/TIFoundationUtils.app +${SRCROOT}/TIModuleName/TIModuleName.app" +``` + +### 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) - [Semantic Commit Messages](docs/semantic-commit-messages.md) - commit message codestyle. - [Snippets](docs/snippets.md) - useful commands and scripts for development. @@ -32,7 +95,7 @@ Useful docs: ./setup ``` -- 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). +- If legacy [Source](https://gitlab.ti/touchinstinct/LeadKit/tree/master/Sources) folder needed, [build dependencies for LeadKit.xcodeproj](https://gitlab.ti/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). @@ -42,14 +105,14 @@ Useful docs: ```swift dependencies: [ - .package(url: "https://github.com/TouchInstinct/LeadKit.git", from: "x.y.z"), + .package(url: "https://gitlab.ti/touchinstinct/LeadKit.git", from: "x.y.z"), ], ``` ### Cocoapods ```ruby -source 'https://github.com/TouchInstinct/Podspecs.git' +source 'https://gitlab.ti/touchinstinct/Podspecs.git' pod 'TISwiftUtils', 'x.y.z' pod 'TIFoundationUtils', 'x.y.z' @@ -58,4 +121,4 @@ 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. \ No newline at end of file +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. diff --git a/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift b/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift index 0cc48109..35d4e8c8 100644 --- a/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift +++ b/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift @@ -24,7 +24,11 @@ import TIMapUtils import MapKit import UIKit -open class AppleClusterPlacemarkManager: BasePlacemarkManager], MKMapRect>, MKMapViewDelegate { +open class AppleClusterPlacemarkManager: BaseClusterPlacemarkManager, + MKMapRect>, + MKMapViewDelegate { + public weak var mapViewDelegate: MKMapViewDelegate? private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self) @@ -36,7 +40,8 @@ open class AppleClusterPlacemarkManager: BasePlacemarkManager: BasePlacemarkManager] else { - return - } + return + } - placemark.image = iconFactory?.markerIcon(for: placemarkManagers) + placemark.image = iconFactory?.markerIcon(for: placemarkManagers, state: .default) } // MARK: - MKMapViewDelegate @@ -79,6 +84,7 @@ open class AppleClusterPlacemarkManager: BasePlacemarkManager: let defaultAnnotationView = placemarkManager.iconFactory != nil ? MKAnnotationView(annotation: annotation, @@ -89,6 +95,7 @@ open class AppleClusterPlacemarkManager: BasePlacemarkManager: BasePlacemarkManager: - _ = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate) + 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: + placemarkManager.state = .default + default: return } diff --git a/TIAppleMapUtils/Sources/AppleMapManager.swift b/TIAppleMapUtils/Sources/AppleMapManager.swift index 4f15d02d..e4994236 100644 --- a/TIAppleMapUtils/Sources/AppleMapManager.swift +++ b/TIAppleMapUtils/Sources/AppleMapManager.swift @@ -44,7 +44,8 @@ open class AppleMapManager: BaseMapManager: BasePlacemarkManager, MKAnnotation { +open class ApplePlacemarkManager: BaseItemPlacemarkManager, + MKAnnotation { + // MARK: - MKAnnotation + + /// A map where all placemarks are placed + public let map: MKMapView - public let coordinate: CLLocationCoordinate2D - + /// 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 state == .default, let annotation = placemark.annotation { + map.deselectAnnotation(annotation, animated: true) + } + + placemark.image = iconFactory?.markerIcon(for: dataModel, state: state) + } + } - public init(dataModel: Model, + public init(map: MKMapView, + dataModel: Model, position: CLLocationCoordinate2D, clusteringIdentifier: String?, iconFactory: AnyMarkerIconFactory?, tapHandler: TapHandlerClosure?) { - self.coordinate = position + self.map = map self.clusteringIdentifier = clusteringIdentifier - super.init(dataModel: dataModel, + super.init(placemarkPosition: position, + dataModel: dataModel, iconFactory: iconFactory, tapHandler: tapHandler) } @@ -47,7 +77,10 @@ open class ApplePlacemarkManager: BasePlacemarkManager 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 387253f9..884ed177 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Login, registration, confirmation and other related actions' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '13.0' s.swift_versions = ['5.3'] diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index 7f8d4b4a..66f400ae 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -2,11 +2,11 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' s.version = '1.33.0' s.summary = 'Deeplink service API' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru', 'castlele' => 'nikita.semenov@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] @@ -14,5 +14,5 @@ Pod::Spec.new do |s| s.source_files = s.name + '/Sources/**/*' s.dependency 'TIFoundationUtils', s.version.to_s - + end diff --git a/TIDeveloperUtils/Sources/DashedBoundsLayer/DashedBoundLayer.swift b/TIDeveloperUtils/Sources/DashedBoundsLayer/DashedBoundLayer.swift new file mode 100644 index 00000000..a631060f --- /dev/null +++ b/TIDeveloperUtils/Sources/DashedBoundsLayer/DashedBoundLayer.swift @@ -0,0 +1,100 @@ +// +// 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 DashedBoundsLayer: CAShapeLayer { + + public static var predefinedColors: [UIColor] { + [.red, .green, .blue, .brown, .gray, .yellow, .magenta, .black, .orange, .purple, .cyan] + } + + private var viewBoundsObservation: NSKeyValueObservation? + + public var dashColor: UIColor = predefinedColors.randomElement() ?? .black + + // MARK: - Open methods + + open func configure(on view: UIView) { + fillColor = UIColor.clear.cgColor + strokeColor = dashColor.cgColor + lineWidth = 1 + lineDashPattern = [4.0, 2.0] + + updateGeometry(from: view) + + view.layer.addSublayer(self) + + viewBoundsObservation = view.observe(\.bounds, options: [.new]) { [weak self] view, _ in + self?.updateGeometry(from: view) + } + } + + open func updateGeometry(from view: UIView) { + frame = view.bounds + path = UIBezierPath(rect: view.bounds).cgPath + } +} + +// MARK: - UIView + DashedBoundsLayer + +public extension UIView { + + @discardableResult + func debugBoundsVisually(debugSubviews: Bool = true) -> UIView { + disableBoundsVisuallyDebug() + + if debugSubviews { + for subview in subviews { + subview.debugBoundsVisually(debugSubviews: debugSubviews) + } + } + + let dashedLayer = DashedBoundsLayer() + dashedLayer.configure(on: self) + + return self + } + + func disableBoundsVisuallyDebug() { + for sublayer in layer.sublayers ?? [] { + if sublayer is DashedBoundsLayer { + sublayer.removeFromSuperlayer() + } + } + + for subview in subviews { + subview.disableBoundsVisuallyDebug() + } + } +} + +// MARK: - UIViewController + DashedBoundsLayer + +public extension UIViewController { + + @discardableResult + func debugBoundsVisually(debugSubviews: Bool = true) -> UIViewController { + view.debugBoundsVisually(debugSubviews: debugSubviews) + return self + } +} diff --git a/TILogging/Sources/LoggingPresenter.swift b/TIDeveloperUtils/Sources/LoggingPresenter/LoggingPresenter.swift similarity index 100% rename from TILogging/Sources/LoggingPresenter.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/LoggingPresenter.swift diff --git a/TILogging/Sources/Views/LoggerList/LogEntryCellView.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerList/LogEntryCellView.swift similarity index 100% rename from TILogging/Sources/Views/LoggerList/LogEntryCellView.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerList/LogEntryCellView.swift diff --git a/TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerList/LogEntryTableViewCell.swift similarity index 100% rename from TILogging/Sources/Views/LoggerList/LogEntryTableViewCell.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerList/LogEntryTableViewCell.swift diff --git a/TILogging/Sources/Views/LoggerList/LogsListViewController.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerList/LogsListViewController.swift similarity index 100% rename from TILogging/Sources/Views/LoggerList/LogsListViewController.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerList/LogsListViewController.swift diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerWindow/LoggingTogglingViewController.swift similarity index 100% rename from TILogging/Sources/Views/LoggerWindow/LoggingTogglingViewController.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerWindow/LoggingTogglingViewController.swift diff --git a/TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerWindow/LoggingTogglingWindow.swift similarity index 100% rename from TILogging/Sources/Views/LoggerWindow/LoggingTogglingWindow.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/LoggerWindow/LoggingTogglingWindow.swift diff --git a/TILogging/Sources/Views/ViewModels/FileCreator.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/FileCreator.swift similarity index 100% rename from TILogging/Sources/Views/ViewModels/FileCreator.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/FileCreator.swift diff --git a/TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift similarity index 100% rename from TILogging/Sources/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/Helpers/DefaultLogsListManipulator.swift diff --git a/TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift similarity index 100% rename from TILogging/Sources/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/Helpers/LogsListManipulatorProtocol.swift diff --git a/TILogging/Sources/Views/ViewModels/LogsListViewOutput.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/LogsListViewOutput.swift similarity index 100% rename from TILogging/Sources/Views/ViewModels/LogsListViewOutput.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/LogsListViewOutput.swift diff --git a/TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift b/TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/LogsStorageViewModel.swift similarity index 100% rename from TILogging/Sources/Views/ViewModels/LogsStorageViewModel.swift rename to TIDeveloperUtils/Sources/LoggingPresenter/Views/ViewModels/LogsStorageViewModel.swift diff --git a/TIDeveloperUtils/Sources/Previews/UIView+Previews.swift b/TIDeveloperUtils/Sources/Previews/UIView+Previews.swift new file mode 100644 index 00000000..0a1aa822 --- /dev/null +++ b/TIDeveloperUtils/Sources/Previews/UIView+Previews.swift @@ -0,0 +1,43 @@ +// +// 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 SwiftUI +import UIKit + +@available(iOS 13, *) +public extension UIView { + private struct Preview: UIViewRepresentable { + let view: UIView + + func makeUIView(context: Context) -> UIView { + view + } + + func updateUIView(_ uiView: UIView, context: Context) { + // + } + } + + func showPreview() -> some View { + Preview(view: self) + } +} diff --git a/TIDeveloperUtils/Sources/Previews/UIViewController+Previews.swift b/TIDeveloperUtils/Sources/Previews/UIViewController+Previews.swift new file mode 100644 index 00000000..e7a4e52c --- /dev/null +++ b/TIDeveloperUtils/Sources/Previews/UIViewController+Previews.swift @@ -0,0 +1,44 @@ +// +// 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 SwiftUI +import UIKit + +@available(iOS 13, *) +public extension UIViewController { + + private struct Preview: UIViewControllerRepresentable { + let viewController: UIViewController + + func makeUIViewController(context: Context) -> UIViewController { + viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + // + } + } + + func showPreview() -> some View { + Preview(viewController: self) + } +} diff --git a/TILogging/TILogging.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec similarity index 58% rename from TILogging/TILogging.podspec rename to TIDeveloperUtils/TIDeveloperUtils.podspec index b0376aac..8c4e076b 100644 --- a/TILogging/TILogging.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |s| - s.name = 'TILogging' - s.version = '1.33.0' - s.summary = 'Logging API' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.name = 'TIDeveloperUtils' + s.version = '1.39.0' + s.summary = 'Universal web view API' + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru', 'castlele' => 'nikita.semenov@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.source_files = s.name + '/Sources/**/*' s.dependency 'TIUIKitCore', s.version.to_s - s.dependency 'TISwiftUtils', s.version.to_s s.dependency 'TIUIElements', s.version.to_s + s.dependency 'TISwiftUtils', s.version.to_s end diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index c752fd0c..f6696364 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Cart, products, promocodes, bonuses and other related actions' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TIFoundationUtils/AsyncOperation/Playground.playground/Contents.swift b/TIFoundationUtils/AsyncOperation/Playground.playground/Contents.swift deleted file mode 100644 index 2cdcf0bf..00000000 --- a/TIFoundationUtils/AsyncOperation/Playground.playground/Contents.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import TIFoundationUtils - -let operationQueue = OperationQueue() -operationQueue.maxConcurrentOperationCount = 1 - -struct NonCancellableTask: CancellableTask { - func cancel() {} -} - -ClosureAsyncOperation { completion in - DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - completion(.success(1)) - } - return NonCancellableTask() - -} -.map { $0 * 2 } -.observe(onSuccess: { result in - debugPrint("Async operation one has finished with \(result)") -}) -.add(to: operationQueue) - -ClosureAsyncOperation { completion in - DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { - completion(.success(2)) - } - return NonCancellableTask() -} -.map { $0 * 2 } -.observe(onSuccess: { result in - debugPrint("Async operation two has finished with \(result)") -}) -.add(to: operationQueue) - -// "Async operation one has finished with 2" -// "Async operation two has finished with 4" - -struct Some { - var arr: [Int] = [] - - mutating func add(_ i: Int) { - arr.append(i) - } -} diff --git a/TIFoundationUtils/AsyncOperation/Playground.playground/contents.xcplayground b/TIFoundationUtils/AsyncOperation/Playground.playground/contents.xcplayground deleted file mode 100644 index cf026f22..00000000 --- a/TIFoundationUtils/AsyncOperation/Playground.playground/contents.xcplayground +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift index 9dadfc85..123fda8c 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift @@ -25,15 +25,25 @@ import Foundation private final class ClosureObserverOperation: DependendAsyncOperation { public typealias OnResultClosure = (Result) -> Void + private let onResult: OnResultClosure? + private let callbackQueue: DispatchQueue + public init(dependency: AsyncOperation, onResult: OnResultClosure? = nil, callbackQueue: DispatchQueue = .main) { - super.init(dependency: dependency) { result in - callbackQueue.async { - onResult?(result) - } - return result + self.onResult = onResult + self.callbackQueue = callbackQueue + + super.init(dependency: dependency) { $0 } + } + + override func handle(result: Result) { + self.result = result + + callbackQueue.async { + self.onResult?(result) + self.state = .isFinished } } } diff --git a/TIFoundationUtils/AsyncOperation/Sources/DependendAsyncOperation.swift b/TIFoundationUtils/AsyncOperation/Sources/DependendAsyncOperation.swift index 6efc6288..2b7848d1 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/DependendAsyncOperation.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/DependendAsyncOperation.swift @@ -24,22 +24,26 @@ import Foundation open class DependendAsyncOperation: AsyncOperation { public var dependencyObservation: NSKeyValueObservation? + private let dependency: Operation public init(dependency: AsyncOperation, resultObservation: @escaping (Result) -> Result) { + self.dependency = dependency super.init() - cancelOnCancellation(of: dependency) + dependency.cancelOnCancellation(of: self) dependencyObservation = dependency.subscribe { [weak self] in - self?.result = resultObservation($0) - self?.state = .isReady + self?.handle(result: resultObservation($0)) } - addDependency(dependency) // keeps strong reference to dependency as well - - state = nil // prevent start of current operation if result is not yet provided by dependency + state = .isReady } + open override func start() { + state = .isExecuting + + dependency.start() + } } diff --git a/TIFoundationUtils/PlaygroundPodfile b/TIFoundationUtils/PlaygroundPodfile new file mode 100644 index 00000000..92fd45c2 --- /dev/null +++ b/TIFoundationUtils/PlaygroundPodfile @@ -0,0 +1,9 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIFoundationUtils' do + platform :ios, 10.0 + use_frameworks! + + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' +end diff --git a/TIFoundationUtils/TIFoundationUtils.app/.gitignore b/TIFoundationUtils/TIFoundationUtils.app/.gitignore new file mode 100644 index 00000000..b7fe13ce --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/.gitignore @@ -0,0 +1,4 @@ +# gitignore nef files +**/build/ +**/nef/ +LICENSE \ No newline at end of file diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/Info.plist b/TIFoundationUtils/TIFoundationUtils.app/Contents/Info.plist new file mode 100644 index 00000000..831ea97a --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + launcher + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + com.fortysevendeg.nef + CFBundleInfoDictionaryVersion + 6.0 + CFBundleSupportedPlatforms + + MacOSX + + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.14 + NSHumanReadableCopyright + Copyright © 2019 The nef Authors. All rights reserved. + + diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/.gitignore b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/.gitignore new file mode 100644 index 00000000..18bd1f3b --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/.gitignore @@ -0,0 +1,26 @@ +## 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 diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile new file mode 100644 index 00000000..92fd45c2 --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/Podfile @@ -0,0 +1,9 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIFoundationUtils' do + platform :ios, 10.0 + use_frameworks! + + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' +end diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Pages/AsyncOperation.xcplaygroundpage/Contents.swift b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Pages/AsyncOperation.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..37a1769a --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Pages/AsyncOperation.xcplaygroundpage/Contents.swift @@ -0,0 +1,77 @@ +/*: + # `AsyncOperation` - generic сабкласс Operation + +Позволяет запускать: + +- асинхронный код внутри операции +- собирать цепочки из операций +- подписываться на результат выполнения + + ## Базовые операции + + "Из коробки", на данный момент, доступен всего один сабкласс асинхронной операции, потому что больше обычно и не нужно. + Но можно наследоваться и создавать собственные сабклассы при необходимости. + */ + +/*: + ### `ClosureAsyncOperation` + + Операция принимающая некий closure, который по окончании своей работы вызовет completion, переданный ему параметром +*/ + +import Foundation +import TIFoundationUtils + +let intResultOperation = ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(3)) { + completion(.success(1)) + } + return Cancellables.nonCancellable() +} + +/*: + ## Базовые операторы + + На данный момент реализовано всего два оператора: + + - `map(mapOutput:mapFailure:)` - конвертирует ResultType в новый NewResultType и ErrorType в новый NewErrorType + - `observe(onSuccess:onFailure)` - просто вызывает переданные callback'и при получении результата или ошибки + + */ + +//: ### Пример запуска асинхронных операци с применением операторов в последовательной очереди и вывод результатов + +let operationQueue = OperationQueue() +operationQueue.maxConcurrentOperationCount = 1 + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(3)) { + completion(.success(1)) + } + return Cancellables.nonCancellable() +} +.map { $0 * 2 } +.observe(onSuccess: { result in + debugPrint("Async operation one has finished with \(result)") +}) +.add(to: operationQueue) + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + completion(.success("Success")) + } + return Cancellables.nonCancellable() +} +.observe(onSuccess: { result in + debugPrint("Async operation two has finished with \(result)") +}) +.add(to: operationQueue) + +/*: + В консоли будет выведено: + + ``` + "Async operation one has finished with 2" + "Async operation two has finished with Success" + ``` + */ diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/contents.xcplayground b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/contents.xcplayground new file mode 100644 index 00000000..00daa653 --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/contents.xcplayground @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/project.pbxproj b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/project.pbxproj new file mode 100644 index 00000000..be06727a --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/project.pbxproj @@ -0,0 +1,396 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A02229F337222E1B9E665195 /* Pods_TIFoundationUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CC8351C190B190691F8FC76 /* Pods_TIFoundationUtils.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0CC8351C190B190691F8FC76 /* Pods_TIFoundationUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIFoundationUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 59A923BF05D59C9BEC19C6FD /* Pods-TIFoundationUtils.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIFoundationUtils.release.xcconfig"; path = "Target Support Files/Pods-TIFoundationUtils/Pods-TIFoundationUtils.release.xcconfig"; sourceTree = ""; }; + 8BACBE8322576CAD00266845 /* TIFoundationUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIFoundationUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EC0CA80D0000D9F6C5FACF34 /* Pods-TIFoundationUtils.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIFoundationUtils.debug.xcconfig"; path = "Target Support Files/Pods-TIFoundationUtils/Pods-TIFoundationUtils.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8BACBE8022576CAD00266845 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A02229F337222E1B9E665195 /* Pods_TIFoundationUtils.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 37058452818BA6D3D6C19AA7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0CC8351C190B190691F8FC76 /* Pods_TIFoundationUtils.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8B39A26221D40F8700DE2643 = { + isa = PBXGroup; + children = ( + 8BACBE8422576CAD00266845 /* TIFoundationUtils */, + 8B39A26C21D40F8700DE2643 /* Products */, + AAAF022BA9D15672298175E6 /* Pods */, + 37058452818BA6D3D6C19AA7 /* Frameworks */, + ); + sourceTree = ""; + }; + 8B39A26C21D40F8700DE2643 /* Products */ = { + isa = PBXGroup; + children = ( + 8BACBE8322576CAD00266845 /* TIFoundationUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + 8BACBE8422576CAD00266845 /* TIFoundationUtils */ = { + isa = PBXGroup; + children = ( + 8BACBE8622576CAD00266845 /* Info.plist */, + ); + path = TIFoundationUtils; + sourceTree = ""; + }; + AAAF022BA9D15672298175E6 /* Pods */ = { + isa = PBXGroup; + children = ( + EC0CA80D0000D9F6C5FACF34 /* Pods-TIFoundationUtils.debug.xcconfig */, + 59A923BF05D59C9BEC19C6FD /* Pods-TIFoundationUtils.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8BACBE7E22576CAD00266845 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8BACBE8222576CAD00266845 /* TIFoundationUtils */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIFoundationUtils" */; + buildPhases = ( + F4F8AAE8BFCE7066005B0EF5 /* [CP] Check Pods Manifest.lock */, + 8BACBE7E22576CAD00266845 /* Headers */, + 8BACBE7F22576CAD00266845 /* Sources */, + 8BACBE8022576CAD00266845 /* Frameworks */, + 8BACBE8122576CAD00266845 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TIFoundationUtils; + productName = TIFoundationUtils2; + productReference = 8BACBE8322576CAD00266845 /* TIFoundationUtils.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 "TIFoundationUtils" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B39A26221D40F8700DE2643; + productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8BACBE8222576CAD00266845 /* TIFoundationUtils */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8BACBE8122576CAD00266845 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + F4F8AAE8BFCE7066005B0EF5 /* [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-TIFoundationUtils-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 = EC0CA80D0000D9F6C5FACF34 /* Pods-TIFoundationUtils.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIFoundationUtils_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIFoundationUtils/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.TIFoundationUtils; + 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 = 59A923BF05D59C9BEC19C6FD /* Pods-TIFoundationUtils.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIFoundationUtils_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIFoundationUtils/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.TIFoundationUtils; + 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 "TIFoundationUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B39A27721D40F8800DE2643 /* Debug */, + 8B39A27821D40F8800DE2643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIFoundationUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BACBE8822576CAD00266845 /* Debug */, + 8BACBE8922576CAD00266845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8B39A26321D40F8700DE2643 /* Project object */; +} diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..e9faedbc --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/xcshareddata/xcschemes/TIFoundationUtils.xcscheme b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/xcshareddata/xcschemes/TIFoundationUtils.xcscheme new file mode 100644 index 00000000..393ee838 --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcodeproj/xcshareddata/xcschemes/TIFoundationUtils.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcworkspace/contents.xcworkspacedata b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..b2b0b0ef --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils/Info.plist b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils/Info.plist new file mode 100644 index 00000000..98d14f60 --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Copyright © 2019. The nef authors. + + diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/launcher b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/launcher new file mode 100755 index 00000000..549f1531 --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/launcher @@ -0,0 +1,6 @@ +#!/bin/bash + +workspace="TIFoundationUtils.xcworkspace" +workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev) + +open "`pwd`/$workspacePath/$workspace" diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/Resources/AppIcon.icns b/TIFoundationUtils/TIFoundationUtils.app/Contents/Resources/AppIcon.icns new file mode 100644 index 00000000..32814f1c Binary files /dev/null and b/TIFoundationUtils/TIFoundationUtils.app/Contents/Resources/AppIcon.icns differ diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/Resources/Assets.car b/TIFoundationUtils/TIFoundationUtils.app/Contents/Resources/Assets.car new file mode 100644 index 00000000..79d9ea89 Binary files /dev/null and b/TIFoundationUtils/TIFoundationUtils.app/Contents/Resources/Assets.car differ diff --git a/TIFoundationUtils/TIFoundationUtils.playground b/TIFoundationUtils/TIFoundationUtils.playground new file mode 120000 index 00000000..89145c90 --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.playground @@ -0,0 +1 @@ +TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground \ No newline at end of file diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index de560ec2..3dd87f16 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,16 +1,23 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Set of helpers for Foundation framework classes.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '10.0' s.swift_versions = ['5.3'] - s.source_files = s.name + '/**/Sources/**/*' + sources = '**/Sources/**/*.swift' + 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 'TISwiftUtils', s.version.to_s s.framework = 'Foundation' diff --git a/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift b/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift index 30bcdda0..376b75da 100644 --- a/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift +++ b/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift @@ -24,7 +24,9 @@ import TIMapUtils import GoogleMapsUtils import GoogleMaps -open class GoogleClusterPlacemarkManager: BasePlacemarkManager], GMSCoordinateBounds>, +open class GoogleClusterPlacemarkManager: BaseClusterPlacemarkManager, + GMSCoordinateBounds>, GMUClusterRendererDelegate, GMUClusterManagerDelegate, GMUClusterIconGenerator { @@ -44,7 +46,8 @@ open class GoogleClusterPlacemarkManager: BasePlacemarkManager: BasePlacemarkManager: clusterItem.configure(placemark: marker) @@ -111,7 +114,7 @@ open class GoogleClusterPlacemarkManager: BasePlacemarkManager Bool { diff --git a/TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift b/TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift index c3e06a57..f8516274 100644 --- a/TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift +++ b/TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift @@ -24,19 +24,34 @@ import TIMapUtils import GoogleMaps import GoogleMapsUtils -open class GooglePlacemarkManager: BasePlacemarkManager, GMUClusterItem { +open class GooglePlacemarkManager: BaseItemPlacemarkManager, + GMUClusterItem { + // MARK: - GMUClusterItem - public let position: CLLocationCoordinate2D + /// Point (coordinates) itself of the current placemark manager + public var position: CLLocationCoordinate2D { + placemarkPosition + } + + /// The current state of a manager's placemark + override public var state: MarkerState { + didSet { + guard let placemark = placemark else { + return + } + + placemark.icon = iconFactory?.markerIcon(for: dataModel, state: state) + } + } public init(dataModel: Model, position: CLLocationCoordinate2D, iconFactory: AnyMarkerIconFactory?, tapHandler: TapHandlerClosure?) { - self.position = position - - super.init(dataModel: dataModel, + super.init(placemarkPosition: position, + dataModel: dataModel, iconFactory: iconFactory, tapHandler: tapHandler) } @@ -44,6 +59,13 @@ open class GooglePlacemarkManager: BasePlacemarkManager 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '12.0' s.swift_versions = ['5.3'] diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 321365d7..25c4fa16 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Set of helpers for Keychain classes.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TILogging/Sources/Logger/Logger.swift b/TILogging/Sources/Logger/Logger.swift deleted file mode 100644 index 07507989..00000000 --- a/TILogging/Sources/Logger/Logger.swift +++ /dev/null @@ -1,77 +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 os - -public struct TILogger: LoggerRepresentable { - - // MARK: - Properties - - @available(iOS 12, *) - public static let defaultLogger = TILogger(subsystem: .defaultSubsystem ?? "", category: .pointsOfInterest) - - public let logInfo: OSLog - - // MARK: - Init - - public init(subsystem: String, category: String) { - self.logInfo = .init(subsystem: subsystem, category: category) - } - - @available(iOS 12, *) - public init(subsystem: String, category: OSLog.Category) { - self.logInfo = .init(subsystem: subsystem, category: category) - } - - // MARK: - LoggerRepresentable - - public func log(_ message: StaticString, log: OSLog?, type: OSLogType, _ arguments: CVarArg...) { - os_log(message, log: log ?? logInfo, type: type, arguments) - } - - // MARK: - Public methods - - public func verbose(_ message: StaticString, _ arguments: CVarArg...) { - self.log(message, log: logInfo, type: .default, arguments) - } - - public func info(_ message: StaticString, _ arguments: CVarArg...) { - self.log(message, log: logInfo, type: .info, arguments) - } - - public func debug(_ message: StaticString, _ arguments: CVarArg...) { - self.log(message, log: logInfo, type: .debug, arguments) - } - - public func error(_ message: StaticString, _ arguments: CVarArg...) { - self.log(message, log: logInfo, type: .error, arguments) - } - - public func fault(_ message: StaticString, _ arguments: CVarArg...) { - self.log(message, log: logInfo, type: .fault, arguments) - } -} - -private extension String { - static let defaultSubsystem = Bundle.main.bundleIdentifier -} diff --git a/TIMapUtils/Sources/Helpers/CoordinateBounds/CLLocationCoordinate2D+Extensions.swift b/TIMapUtils/Sources/Helpers/CoordinateBounds/CLLocationCoordinate2D+Extensions.swift new file mode 100644 index 00000000..a2916a26 --- /dev/null +++ b/TIMapUtils/Sources/Helpers/CoordinateBounds/CLLocationCoordinate2D+Extensions.swift @@ -0,0 +1,29 @@ +// +// 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 CoreLocation + +extension CLLocationCoordinate2D: Equatable { + public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { + lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude + } +} diff --git a/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift index c6a9da8a..ccebd849 100644 --- a/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/AnyMarkerIconFactory.swift @@ -23,20 +23,20 @@ import UIKit.UIImage public final class AnyMarkerIconFactory: MarkerIconFactory { - public typealias IconProviderClosure = (Model) -> UIImage + public typealias IconProviderClosure = (Model, MarkerState) -> UIImage public var iconProviderClosure: IconProviderClosure public init(iconFactory: IF) where IF.Model == Model { - self.iconProviderClosure = { iconFactory.markerIcon(for: $0) } + self.iconProviderClosure = { iconFactory.markerIcon(for: $0, state: $1) } } public init(iconFactory: IF, transform: @escaping (Model) -> T) where IF.Model == T { - self.iconProviderClosure = { iconFactory.markerIcon(for: transform($0)) } + self.iconProviderClosure = { iconFactory.markerIcon(for: transform($0), state: $1) } } - public func markerIcon(for model: Model) -> UIImage { - iconProviderClosure(model) + public func markerIcon(for model: Model, state: MarkerState) -> UIImage { + iconProviderClosure(model, state) } } diff --git a/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift index 10083d31..b29381d8 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift @@ -23,7 +23,7 @@ import UIKit.UIImage open class DefaultCachableMarkerIconFactory: DefaultMarkerIconFactory { - public typealias CacheKeyProvider = (M) -> K + public typealias CacheKeyProvider = (M, MarkerState) -> K public let cache = NSCache() @@ -37,11 +37,11 @@ open class DefaultCachableMarkerIconFactory: DefaultMarkerIconF super.init(createIconClosure: createIconClosure) } - open override func markerIcon(for model: M) -> UIImage { - let cacheKey = cacheKeyProvider(model) + open override func markerIcon(for model: M, state: MarkerState) -> UIImage { + let cacheKey = cacheKeyProvider(model, state) guard let cachedIcon = cache.object(forKey: cacheKey) else { - let icon = super.markerIcon(for: model) + let icon = super.markerIcon(for: model, state: state) cache.setObject(icon, forKey: cacheKey) return icon diff --git a/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift index c2ab65b7..04e800dc 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultClusterMarkerIconFactory.swift @@ -32,12 +32,12 @@ public final class DefaultClusterMarkerIconFactory: DefaultCachableMarker self.beforeRenderCallback = beforeRenderCallback self.clusterIconRenderer = DefaultClusterIconRenderer() - super.init { [clusterIconRenderer] in - beforeRenderCallback?($0, clusterIconRenderer) + super.init { [clusterIconRenderer] models, _ in + beforeRenderCallback?(models, clusterIconRenderer) - return clusterIconRenderer.renderCluster(of: $0.count) - } cacheKeyProvider: { - String($0.count) as NSString + return clusterIconRenderer.renderCluster(of: models.count) + } cacheKeyProvider: { models, _ in + String(models.count) as NSString } } } diff --git a/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift index b4d4b35a..2fb403f5 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift @@ -23,7 +23,7 @@ import UIKit.UIImage open class DefaultMarkerIconFactory: MarkerIconFactory { - public typealias CreateIconClosure = (M) -> UIImage + public typealias CreateIconClosure = (M, MarkerState) -> UIImage private let createIconClosure: CreateIconClosure @@ -31,8 +31,8 @@ open class DefaultMarkerIconFactory: MarkerIconFactory { self.createIconClosure = createIconClosure } - open func markerIcon(for model: M) -> UIImage { - postprocess(icon: createIconClosure(model)) + open func markerIcon(for model: M, state: MarkerState) -> UIImage { + postprocess(icon: createIconClosure(model, state)) } open func postprocess(icon: UIImage) -> UIImage { diff --git a/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift index 405688bb..919e338c 100644 --- a/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift @@ -25,5 +25,5 @@ import UIKit.UIImage public protocol MarkerIconFactory { associatedtype Model - func markerIcon(for model: Model) -> UIImage + func markerIcon(for model: Model, state: MarkerState) -> UIImage } diff --git a/TIMapUtils/Sources/IconProviders/MarkerState.swift b/TIMapUtils/Sources/IconProviders/MarkerState.swift new file mode 100644 index 00000000..c21f9762 --- /dev/null +++ b/TIMapUtils/Sources/IconProviders/MarkerState.swift @@ -0,0 +1,31 @@ +// +// 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. +// + +/// Available marker states on any map +public enum MarkerState: String { + + /// A state where a map point is selected and a marker is highlighted + case selected + + /// A default state where a map point is shown on a map + case `default` +} diff --git a/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift b/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift index 8e28bdbe..65fad0aa 100644 --- a/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift +++ b/TIMapUtils/Sources/IconProviders/StaticImageIconFactory.swift @@ -29,7 +29,7 @@ public final class StaticImageIconFactory: MarkerIconFactory { self.image = image } - public func markerIcon(for model: Model) -> UIImage { + public func markerIcon(for model: Model, state: MarkerState) -> UIImage { image } } diff --git a/TIMapUtils/Sources/Managers/BaseClusterPlacemarkManager.swift b/TIMapUtils/Sources/Managers/BaseClusterPlacemarkManager.swift new file mode 100644 index 00000000..fa1f5aa8 --- /dev/null +++ b/TIMapUtils/Sources/Managers/BaseClusterPlacemarkManager.swift @@ -0,0 +1,43 @@ +// +// 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. +// + +/** + Base cluster placemark manager + + - Parameters: + - ClusterPlacemark: a generic parameter describing a cluster placemark itself + - ItemPlacemarkManager: a single placemarks manager element of array for a cluster + - ClusterBounds: a rectangle area of the current cluster or a collection of cluster placemark objects + */ +open class BaseClusterPlacemarkManager: + BasePlacemarkManager where ItemPlacemarkManager.Position : Equatable { + + /// Manual selecting of a placemark with an incoming point coordinates + open func selectMarker(with point: ItemPlacemarkManager.Position) { + dataModel.filter { $0.placemarkPosition == point }.forEach { $0.state = .selected } + } + + /// Manual state resetting of all placemarks with currently selected state + open func resetMarkersState() { + dataModel.filter { $0.state == .selected }.forEach { $0.state = .default } + } +} diff --git a/TIMapUtils/Sources/Managers/BaseItemPlacemarkManager.swift b/TIMapUtils/Sources/Managers/BaseItemPlacemarkManager.swift new file mode 100644 index 00000000..15f9b45a --- /dev/null +++ b/TIMapUtils/Sources/Managers/BaseItemPlacemarkManager.swift @@ -0,0 +1,43 @@ +// +// 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. +// + +/** + Base single item placemark manager + + Contains all properties of `BasePlacemarkManager` and adds a `placemark` property to use + + - Parameters: + - Placemark: a placemark itself managed by an item placemark manager + - DataModel: a data model of a placemark which is used for configuration etc. + - Location: latitude and longitude of a placemark + */ +open class BaseItemPlacemarkManager: BasePlacemarkManager { + + /// Placemark itself of the current placemark manager + public private(set) var placemark: Placemark? + + override open func configure(placemark: Placemark) { + super.configure(placemark: placemark) + + self.placemark = placemark + } +} diff --git a/TIMapUtils/Sources/Managers/BaseMapManager.swift b/TIMapUtils/Sources/Managers/BaseMapManager.swift index a8dc5b37..43345bf0 100644 --- a/TIMapUtils/Sources/Managers/BaseMapManager.swift +++ b/TIMapUtils/Sources/Managers/BaseMapManager.swift @@ -23,7 +23,7 @@ import Foundation import UIKit.UIGeometry -open class BaseMapManager where PM.Position: LocationCoordinate, @@ -79,16 +79,20 @@ open class BaseMapManager: NSObject, PlacemarkManager, PlacemarkConfigurator { + public typealias TapHandlerClosure = (DataModel, Location) -> Bool + /// The current state of a manager's placemark + open var state: MarkerState = .default + + /// Point (coordinates) itself of the current placemark manager + public let placemarkPosition: Location + + /// Model for the current placemark manager + public let dataModel: DataModel + public var tapHandler: TapHandlerClosure? public var iconFactory: AnyMarkerIconFactory? - public let dataModel: DataModel - - public init(dataModel: DataModel, + public init(placemarkPosition: Location, + dataModel: DataModel, iconFactory: AnyMarkerIconFactory?, tapHandler: TapHandlerClosure?) { + self.placemarkPosition = placemarkPosition self.dataModel = dataModel self.iconFactory = iconFactory self.tapHandler = tapHandler diff --git a/TIMapUtils/Sources/Managers/PlacemarkManager.swift b/TIMapUtils/Sources/Managers/PlacemarkManager.swift index b58ad67f..9be97560 100644 --- a/TIMapUtils/Sources/Managers/PlacemarkManager.swift +++ b/TIMapUtils/Sources/Managers/PlacemarkManager.swift @@ -20,12 +20,24 @@ // THE SOFTWARE. // -public protocol PlacemarkManager { +public protocol PlacemarkManager: AnyObject { associatedtype DataModel associatedtype Position typealias TapHandlerClosure = (DataModel, Position) -> Bool + var placemarkPosition: Position { get } var dataModel: DataModel { get } + var state: MarkerState { get set } + + /// + /// Validates whether the current tap could be handled or not + /// + /// - Parameters: + /// - DataModel: A data model of the current placemark manager + /// - Position: A position of the current placemark + /// + /// - Returns: A `Bool` value of the handling desicion + /// var tapHandler: TapHandlerClosure? { get set } } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index b91c2ed7..94019482 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Set of helpers for map objects clustering and interacting.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '10.0' s.swift_versions = ['5.3'] diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index d394f56f..c8d020a5 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Moya + Swagger network service.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 0aa168f9..e26aa3b6 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Swagger-frendly networking layer helpers.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '10.0' s.swift_versions = ['5.3'] diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index e9be3cd8..3292f422 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Caching results of EndpointRequests.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index c8ff1597..c2327dde 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Generic pagination component.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '10.0' s.swift_versions = ['5.3'] diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 76ae3c2d..cad3bd44 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Core UI elements: protocols, views and helpers.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '13.0' s.swift_versions = ['5.3'] diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index f9b76474..f610dc08 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,14 +1,21 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Bunch of useful helpers for Swift development.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '9.0' s.swift_versions = ['5.3'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*.swift' + 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 end diff --git a/TITableKitUtils/Sources/Extensions/Appearance/TableRow+AppearanceConfigurable.swift b/TITableKitUtils/Sources/Extensions/Appearance/TableRow+AppearanceConfigurable.swift new file mode 100644 index 00000000..bdedb3d6 --- /dev/null +++ b/TITableKitUtils/Sources/Extensions/Appearance/TableRow+AppearanceConfigurable.swift @@ -0,0 +1,46 @@ +// +// 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 TableKit +import TIUIKitCore + +extension TableRow: AppearanceConfigurable where CellType: AppearanceConfigurable { + private static var configureAppearanceActionId: String { + "TableRowConfigureAppearanceActionId" + } + + public func with(appearance: CellType.Appearance) -> Self { + configure(appearance: appearance) + return self + } + + public func configure(appearance: CellType.Appearance) { + removeAction(forActionId: Self.configureAppearanceActionId) + + let action = TableRowAction(.configure) { options in + options.cell?.configure(appearance: appearance) + } + + action.id = Self.configureAppearanceActionId + on(action) + } +} diff --git a/TITableKitUtils/Sources/Extensions/Appearance/WrappableView+TableViewContainers.swift b/TITableKitUtils/Sources/Extensions/Appearance/WrappableView+TableViewContainers.swift new file mode 100644 index 00000000..80b170f2 --- /dev/null +++ b/TITableKitUtils/Sources/Extensions/Appearance/WrappableView+TableViewContainers.swift @@ -0,0 +1,35 @@ +// +// 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 TableKit +import TIUIElements +import TIUIKitCore +import UIKit + +public extension WrappableView where Self: ConfigurableView { + typealias InTableRow = TableKit.TableRow + typealias InSeparatableRow = TableKit.TableRow +} + +public extension WrappableView where Self: UITableViewCell & ConfigurableCell { + typealias TableRow = TableKit.TableRow +} diff --git a/TITableKitUtils/Sources/Separators/Extensions/Array/Array+SeparatorRowBox.swift b/TITableKitUtils/Sources/Separators/Extensions/Array/Array+SeparatorRowBox.swift index fa212ca2..623047a7 100644 --- a/TITableKitUtils/Sources/Separators/Extensions/Array/Array+SeparatorRowBox.swift +++ b/TITableKitUtils/Sources/Separators/Extensions/Array/Array+SeparatorRowBox.swift @@ -31,23 +31,23 @@ public extension Array where Element == SeparatorRowBox { } /// Configure separators from SeparatorRowBox array - /// - parameter extreme: Configuration that will be used for extreme values, for first or last row - /// - parameter middle: Configuration for intermediate rows - func configureSeparators(extreme extremeSeparatorConfiguration: SeparatorConfiguration, - middle middleSeparatorConfiguration: SeparatorConfiguration) { + /// - parameter extreme: Appearance that will be used for extreme values, for first or last row + /// - parameter middle: Apearance for intermediate rows + func configureSeparators(extreme extremeSeparatorsAppearance: SeparatorAppearance, + middle middleSeparatorsAppearance: SeparatorAppearance) { - configureSeparators(first: extremeSeparatorConfiguration, - middle: middleSeparatorConfiguration, - last: extremeSeparatorConfiguration) + configureSeparators(first: extremeSeparatorsAppearance, + middle: middleSeparatorsAppearance, + last: extremeSeparatorsAppearance) } /// Configure separators from SeparatorRowBox array - /// - parameter first: Configuration of the top separator of the first row - /// - parameter middle: Configuration of the separators between the rows - /// - parameter last: Configuration of the bottom separator of the last row - func configureSeparators(first firstSeparatorConfiguration: SeparatorConfiguration, - middle middleSeparatorConfiguration: SeparatorConfiguration, - last lastSeparatorConfiguration: SeparatorConfiguration) { + /// - parameter first: Appearance of the top separator of the first row + /// - parameter middle: Appearance of the separators between the rows + /// - parameter last: Appearance of the bottom separator of the last row + func configureSeparators(first firstSeparatorsAppearance: SeparatorAppearance, + middle middleSeparatorsAppearance: SeparatorAppearance, + last lastSeparatorsAppearance: SeparatorAppearance) { if isEmpty { return @@ -55,12 +55,12 @@ public extension Array where Element == SeparatorRowBox { switch count { case 1: - first?.set(separatorType: .full(firstSeparatorConfiguration, lastSeparatorConfiguration)) + first?.set(separatorType: .full(top: firstSeparatorsAppearance, bottom: lastSeparatorsAppearance)) default: - dropFirst().dropLast().forEach { $0.set(separatorType: .bottom(middleSeparatorConfiguration)) } - first?.set(separatorType: .full(firstSeparatorConfiguration, middleSeparatorConfiguration)) - last?.set(separatorType: .bottom(lastSeparatorConfiguration)) + dropFirst().dropLast().forEach { $0.set(separatorType: .bottom(middleSeparatorsAppearance)) } + first?.set(separatorType: .full(top: firstSeparatorsAppearance, bottom: middleSeparatorsAppearance)) + last?.set(separatorType: .bottom(lastSeparatorsAppearance)) } } } diff --git a/TITableKitUtils/Sources/Separators/Extensions/ContainerTableViewCell+Configurable.swift b/TITableKitUtils/Sources/Separators/Extensions/ContainerTableViewCell+Configurable.swift new file mode 100644 index 00000000..64f618bf --- /dev/null +++ b/TITableKitUtils/Sources/Separators/Extensions/ContainerTableViewCell+Configurable.swift @@ -0,0 +1,31 @@ +// +// 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 TableKit +import TIUIElements +import TIUIKitCore + +extension ContainerTableViewCell: ConfigurableCell where View: ConfigurableView { + public func configure(with viewModel: View.ViewModelType) { + wrappedView.configure(with: viewModel) + } +} diff --git a/TITableKitUtils/Sources/Separators/Extensions/TableRow/TableRow+Separators.swift b/TITableKitUtils/Sources/Separators/Extensions/TableRow/TableRow+Separators.swift index 05ae4742..41a9ac58 100644 --- a/TITableKitUtils/Sources/Separators/Extensions/TableRow/TableRow+Separators.swift +++ b/TITableKitUtils/Sources/Separators/Extensions/TableRow/TableRow+Separators.swift @@ -23,28 +23,29 @@ import TableKit import TIUIElements -private let configureSeparatorActionId = "TableRowConfigureSeparatorActionId" +extension TableRow: SeparatorsConfigurable where CellType: SeparatorsConfigurable { + private static var configureSeparatorsActionId: String { + "TableRowConfigureSeparatorsActionId" + } -public extension TableRow where CellType: SeparatorConfigurable { - - func with(separatorType: ViewSeparatorType) -> Self { - set(separatorType: separatorType) + public func with(separators: SeparatorsConfiguration) -> Self { + configureSeparators(with: separators) return self } - func set(separatorType: ViewSeparatorType) { - removeAction(forActionId: configureSeparatorActionId) + public func configureSeparators(with separatorsConfiguration: SeparatorsConfiguration) { + removeAction(forActionId: Self.configureSeparatorsActionId) - let action = TableRowAction(.configure) { - $0.cell?.configureSeparators(with: separatorType) + let action = TableRowAction(.configure) { options in + options.cell?.configureSeparators(with: separatorsConfiguration) } - action.id = configureSeparatorActionId + action.id = Self.configureSeparatorsActionId on(action) } } -public extension TableRow where CellType: SeparatorConfigurable { +public extension TableRow where CellType: SeparatorsConfigurable { /// TableRow typed as SeparatorRowBox var separatorRowBox: SeparatorRowBox { diff --git a/TITableKitUtils/Sources/Separators/SeparatorRowBox.swift b/TITableKitUtils/Sources/Separators/SeparatorRowBox.swift index 6109c4da..0c8f5cc7 100644 --- a/TITableKitUtils/Sources/Separators/SeparatorRowBox.swift +++ b/TITableKitUtils/Sources/Separators/SeparatorRowBox.swift @@ -21,21 +21,21 @@ // import TableKit -import TIUIElements import TISwiftUtils +import TIUIElements /// Class that used to configure separators when multiply cells presented in one section public final class SeparatorRowBox { - private let setSeparatorHandler: ParameterClosure + private let setSeparatorHandler: ParameterClosure - public func set(separatorType: ViewSeparatorType) { + public func set(separatorType: SeparatorsConfiguration) { setSeparatorHandler(separatorType) } public let row: Row - public init(row: TableRow) where T: SeparatorConfigurable { + public init(row: TableRow) where T: SeparatorsConfigurable { self.row = row - setSeparatorHandler = row.set + setSeparatorHandler = row.configureSeparators(with:) } } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index 73456a50..e7cdf5c4 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Set of helpers for TableKit classes.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TITransitions/TITransitions.podspec b/TITransitions/TITransitions.podspec index e20b753a..39a30619 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'TITransitions' - s.version = '1.33.0' + s.version = '1.34.1' s.summary = 'Set of custom transitions to present controller. ' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'Loupehope' => 'vladislav.suhomlinov@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.0'] diff --git a/TIUIElements/PlaygroundPodfile b/TIUIElements/PlaygroundPodfile new file mode 100644 index 00000000..95938d81 --- /dev/null +++ b/TIUIElements/PlaygroundPodfile @@ -0,0 +1,10 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIUIElements' do + platform :ios, 11.0 + use_frameworks! + + pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec' + pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' +end diff --git a/TIUIElements/Sources/Appearance/UIButton+Appearance.swift b/TIUIElements/Sources/Appearance/UIButton+Appearance.swift new file mode 100644 index 00000000..971606dc --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIButton+Appearance.swift @@ -0,0 +1,60 @@ +// +// 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 +import UIKit + +extension UIButton { + open class BaseAppearance: UIView.BaseAppearance { + + public var textAttributes: BaseTextAttributes? + public var contentInsets: UIEdgeInsets + public var titleInsets: UIEdgeInsets + public var imageInsets: UIEdgeInsets + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + textAttributes: BaseTextAttributes? = nil, + contentInsets: UIEdgeInsets = .zero, + titleInsets: UIEdgeInsets = .zero, + imageInsets: UIEdgeInsets = .zero) { + + self.textAttributes = textAttributes + self.contentInsets = contentInsets + self.titleInsets = titleInsets + self.imageInsets = imageInsets + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } + } + + public final class DefaultAppearance: BaseAppearance, WrappedViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } +} diff --git a/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift b/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift new file mode 100644 index 00000000..667bf059 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift @@ -0,0 +1,53 @@ +// +// 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 +import UIKit + +extension UIButton { + public func configureUIButton(appearance: UIButton.BaseAppearance) { + appearance.textAttributes? + .configure(button: self, + with: titleLabel?.attributedText?.string ?? titleLabel?.text) + + if #available(iOS 15, *) { + configuration?.contentInsets = .init(insets: appearance.contentInsets) + + if configuration?.imagePlacement == .leading { + let padding = appearance.titleInsets.left + appearance.imageInsets.right + configuration?.imagePadding = padding + } + + if configuration?.imagePlacement == .trailing { + let padding = appearance.titleInsets.right + appearance.imageInsets.left + configuration?.imagePadding = padding + } + + } else { + contentEdgeInsets = appearance.contentInsets + titleEdgeInsets = appearance.titleInsets + imageEdgeInsets = appearance.imageInsets + } + + super.configureUIView(appearance: appearance) + } +} diff --git a/TIUIElements/Sources/Appearance/UILabel+Appearance.swift b/TIUIElements/Sources/Appearance/UILabel+Appearance.swift new file mode 100644 index 00000000..176c2389 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UILabel+Appearance.swift @@ -0,0 +1,51 @@ +// +// 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 +import UIKit + +extension UILabel { + open class BaseAppearance: UIView.BaseAppearance { + + public var textAttributes: BaseTextAttributes? + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + textAttributes: BaseTextAttributes? = nil) { + + self.textAttributes = textAttributes + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } + } + + public final class DefaultAppearance: BaseAppearance, WrappedViewAppearance { + public static var defaultAppearance: DefaultAppearance { + DefaultAppearance() + } + } +} diff --git a/TIUIElements/Sources/Separators/SeparatorConfiguration.swift b/TIUIElements/Sources/Appearance/UILabel+AppearanceConfigurable.swift similarity index 76% rename from TIUIElements/Sources/Separators/SeparatorConfiguration.swift rename to TIUIElements/Sources/Appearance/UILabel+AppearanceConfigurable.swift index 0a44231c..394b09d7 100644 --- a/TIUIElements/Sources/Separators/SeparatorConfiguration.swift +++ b/TIUIElements/Sources/Appearance/UILabel+AppearanceConfigurable.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Touch Instinct +// 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 @@ -20,17 +20,15 @@ // THE SOFTWARE. // +import TIUIKitCore import UIKit -public struct SeparatorConfiguration { +extension UILabel { + public func configureUILabel(appearance: BaseAppearance) { + appearance.textAttributes? + .configure(label: self, + with: attributedText?.string ?? text) - public let color: UIColor - public let insets: UIEdgeInsets - public let height: CGFloat - - public init(color: UIColor, insets: UIEdgeInsets = .zero, height: CGFloat = 1) { - self.color = color - self.insets = insets - self.height = height + super.configureUIView(appearance: appearance) } } diff --git a/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift b/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift new file mode 100644 index 00000000..7f016266 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIVIew+AppearanceConfigurable.swift @@ -0,0 +1,45 @@ +// +// 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 +import UIKit + +extension UIView { + public func configureUIView(appearance: BaseAppearance) { + backgroundColor = appearance.backgroundColor + layer.masksToBounds = true + layer.maskedCorners = appearance.border.roundedCorners + layer.cornerRadius = appearance.border.cornerRadius + layer.borderWidth = appearance.border.width + layer.borderColor = appearance.border.color.cgColor + + guard let shadow = appearance.shadow else { + return + } + + layer.shadowOpacity = shadow.opacity + layer.shadowOffset = shadow.offset + layer.shadowColor = shadow.color.cgColor + layer.shadowRadius = shadow.radius + clipsToBounds = false + } +} diff --git a/TIUIElements/Sources/Appearance/UIView+Appearance.swift b/TIUIElements/Sources/Appearance/UIView+Appearance.swift new file mode 100644 index 00000000..b2ac86d0 --- /dev/null +++ b/TIUIElements/Sources/Appearance/UIView+Appearance.swift @@ -0,0 +1,164 @@ +// +// 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 +import UIKit + +extension UIView { + + // MARK: - Layout Variations + + public struct NoLayout: ViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + open class BaseSizeLayout { + public var size: CGSize + + public init(size: CGSize = .infinity) { + self.size = size + } + } + + public final class DefaultLayout: BaseSizeLayout, SizeViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + // MARK: - WrappedView Layout + + open class BaseWrappedLayout: BaseSizeLayout { + public var centerOffset: UIOffset + public var insets: UIEdgeInsets + + public init(insets: UIEdgeInsets = .zero, + size: CGSize = .infinity, + centerOffset: UIOffset = .nan) { + + self.centerOffset = centerOffset + self.insets = insets + + super.init(size: size) + } + } + + open class BaseSpecedWrappedLayout: BaseWrappedLayout { + public var spacing: CGFloat + + public init(insets: UIEdgeInsets = .zero, + size: CGSize = .infinity, + centerOffset: UIOffset = .nan, + spacing: CGFloat = .zero) { + self.spacing = spacing + + super.init(insets: insets, size: size, centerOffset: centerOffset) + } + } + + public final class DefaultWrappedLayout: BaseWrappedLayout, WrappedViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + public final class DefaultSpacedWrappedLayout: BaseSpecedWrappedLayout, SpacedWrappedViewLayout { + public static var defaultLayout: Self { + Self() + } + } + + // MARK: - Appearance Variations + + open class BaseAppearance { + public var layout: Layout + + public var backgroundColor: UIColor + public var border: UIViewBorder + public var shadow: UIViewShadow? + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil) { + + self.layout = layout + self.backgroundColor = backgroundColor + self.border = border + self.shadow = shadow + } + } + + public final class DefaultAppearance: BaseAppearance, ViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } + + // MARK: - WrappedView Appearance + + open class BaseWrappedViewHolderAppearance: BaseAppearance { + + public var subviewAppearance: SubviewAppearance + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + subviewAppearance: SubviewAppearance = .defaultAppearance) { + + self.subviewAppearance = subviewAppearance + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } + } + + public final class DefaultWrappedViewHolderAppearance: BaseWrappedViewHolderAppearance, + WrappedViewHolderAppearance { + public static var defaultAppearance: Self { + Self() + } + } + + public final class DefaultWrappedAppearance: BaseAppearance, WrappedViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } + + public final class DefaultSpacedWrappedAppearance: BaseAppearance, WrappedViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } +} + +extension UIView.DefaultWrappedViewHolderAppearance: WrappedViewAppearance where Layout: WrappedViewLayout { + +} diff --git a/TIUIElements/Sources/Separators/BaseSeparatorCell.swift b/TIUIElements/Sources/Separators/ContainerSeparatorTableViewCell.swift similarity index 71% rename from TIUIElements/Sources/Separators/BaseSeparatorCell.swift rename to TIUIElements/Sources/Separators/ContainerSeparatorTableViewCell.swift index c426a46c..1f34f887 100644 --- a/TIUIElements/Sources/Separators/BaseSeparatorCell.swift +++ b/TIUIElements/Sources/Separators/ContainerSeparatorTableViewCell.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Touch Instinct +// 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 @@ -22,7 +22,8 @@ import UIKit -open class BaseSeparatorCell: BaseInitializableCell, SeparatorConfigurable { +open class ContainerSeparatorTableViewCell: ContainerTableViewCell, SeparatorsConfigurable { + private lazy var topSeparatorView = createTopSeparator() private lazy var bottomSeparatorView = createBottomSeparator() @@ -52,23 +53,25 @@ open class BaseSeparatorCell: BaseInitializableCell, SeparatorConfigurable { contentView.addSubview(bottomSeparatorView) } - public func configureSeparators(with separatorType: ViewSeparatorType) { - topSeparatorView.isHidden = separatorType.topIsHidden - bottomSeparatorView.isHidden = separatorType.bottomIsHidden + // MARK: - SeparatorsConfigurable - switch separatorType { + public func configureSeparators(with separatorsConfiguration: SeparatorsConfiguration) { + topSeparatorView.isHidden = separatorsConfiguration.topIsHidden + bottomSeparatorView.isHidden = separatorsConfiguration.bottomIsHidden + + switch separatorsConfiguration { case .none: break - case let .bottom(configuration): - updateBottomSeparator(with: configuration) + case let .bottom(appearance): + updateBottomSeparator(with: appearance) - case let .top(configuration): - updateTopSeparator(with: configuration) + case let .top(appearance): + updateTopSeparator(with: appearance) - case let .full(topConfiguration, bottomConfiguration): - updateTopSeparator(with: topConfiguration) - updateBottomSeparator(with: bottomConfiguration) + case let .full(topAppearance, bottomAppearance): + updateTopSeparator(with: topAppearance) + updateBottomSeparator(with: bottomAppearance) } } @@ -127,26 +130,26 @@ open class BaseSeparatorCell: BaseInitializableCell, SeparatorConfigurable { $0.translatesAutoresizingMaskIntoConstraints = false } } -} -private extension BaseSeparatorCell { - func updateTopSeparator(with configuration: SeparatorConfiguration) { - topSeparatorView.backgroundColor = configuration.color + // MARK: - Private - topViewHeightConstraint?.constant = configuration.height + private func updateTopSeparator(with appearance: SeparatorAppearance) { + topSeparatorView.configureUIView(appearance: appearance) - topViewTopConstraint?.constant = configuration.insets.top - topViewLeftConstraint?.constant = configuration.insets.left - topViewRightConstraint?.constant = configuration.insets.right + topViewHeightConstraint?.constant = appearance.layout.size.height + + topViewTopConstraint?.constant = appearance.layout.insets.top + topViewLeftConstraint?.constant = appearance.layout.insets.left + topViewRightConstraint?.constant = appearance.layout.insets.right } - func updateBottomSeparator(with configuration: SeparatorConfiguration) { - bottomSeparatorView.backgroundColor = configuration.color + private func updateBottomSeparator(with appearance: SeparatorAppearance) { + bottomSeparatorView.configureUIView(appearance: appearance) - bottomViewHeightConstraint?.constant = configuration.height + bottomViewHeightConstraint?.constant = appearance.layout.size.height - bottomViewBottomConstraint?.constant = configuration.insets.bottom - bottomViewLeftConstraint?.constant = configuration.insets.left - bottomViewRightConstraint?.constant = configuration.insets.right + bottomViewBottomConstraint?.constant = appearance.layout.insets.bottom + bottomViewLeftConstraint?.constant = appearance.layout.insets.left + bottomViewRightConstraint?.constant = appearance.layout.insets.right } } diff --git a/TIUIElements/Sources/Separators/SeparatorAppearance.swift b/TIUIElements/Sources/Separators/SeparatorAppearance.swift new file mode 100644 index 00000000..ee4914f0 --- /dev/null +++ b/TIUIElements/Sources/Separators/SeparatorAppearance.swift @@ -0,0 +1,44 @@ +// +// 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 +import UIKit + +public final class SeparatorLayout: UIView.BaseWrappedLayout, WrappedViewLayout { + public static var defaultLayout: Self { + Self() + } + + public init(insets: UIEdgeInsets = .zero, + size: CGSize = .fixedHeight(0.5)) { + + super.init(insets: insets, + size: size, + centerOffset: .nan) + } +} + +public final class SeparatorAppearance: UIView.BaseAppearance, ViewAppearance { + public static var defaultAppearance: Self { + Self(backgroundColor: .lightGray) + } +} diff --git a/TIUIElements/Sources/Separators/SeparatorConfigurable.swift b/TIUIElements/Sources/Separators/SeparatorConfigurable.swift index 6e58a390..1bb3fb6a 100644 --- a/TIUIElements/Sources/Separators/SeparatorConfigurable.swift +++ b/TIUIElements/Sources/Separators/SeparatorConfigurable.swift @@ -20,6 +20,6 @@ // THE SOFTWARE. // -public protocol SeparatorConfigurable { - func configureSeparators(with separatorType: ViewSeparatorType) +public protocol SeparatorsConfigurable { + func configureSeparators(with separatorsConfiguration: SeparatorsConfiguration) } diff --git a/TIUIElements/Sources/Separators/ViewSeparatorType.swift b/TIUIElements/Sources/Separators/SeparatorsConfiguration.swift similarity index 79% rename from TIUIElements/Sources/Separators/ViewSeparatorType.swift rename to TIUIElements/Sources/Separators/SeparatorsConfiguration.swift index 9158ff2e..d312439b 100644 --- a/TIUIElements/Sources/Separators/ViewSeparatorType.swift +++ b/TIUIElements/Sources/Separators/SeparatorsConfiguration.swift @@ -20,22 +20,14 @@ // THE SOFTWARE. // -public enum ViewSeparatorType { - - /// All separators for view is hidden +public enum SeparatorsConfiguration { case none - - /// Show only top separator - case top(SeparatorConfiguration) - - /// Show only bottom separator - case bottom(SeparatorConfiguration) - - /// First configuration for top, second for bottom - case full(SeparatorConfiguration, SeparatorConfiguration) + case top(SeparatorAppearance) + case bottom(SeparatorAppearance) + case full(top: SeparatorAppearance, bottom: SeparatorAppearance) } -public extension ViewSeparatorType { +public extension SeparatorsConfiguration { /// Determine if bottom separator is hidden. var bottomIsHidden: Bool { @@ -48,7 +40,7 @@ public extension ViewSeparatorType { } /// Returns top configuration if type is top or full. - var topConfiguration: SeparatorConfiguration? { + var topConfiguration: SeparatorAppearance? { switch self { case let .top(configuration), let .full(configuration, _): return configuration @@ -59,7 +51,7 @@ public extension ViewSeparatorType { } /// Returns bottom configuration if type is bottom or full. - var bottomConfiguration: SeparatorConfiguration? { + var bottomConfiguration: SeparatorAppearance? { switch self { case let .bottom(configuration), let .full(_, configuration): return configuration diff --git a/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift b/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift new file mode 100644 index 00000000..e9881d1b --- /dev/null +++ b/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift @@ -0,0 +1,86 @@ +// +// 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 +import UIKit + +extension WrappedViewLayout { + func setupSize(widthConstraint: NSLayoutConstraint?, + heightConstraint: NSLayoutConstraint?) { + + if size.width.isFinite { + widthConstraint?.constant = size.width + widthConstraint?.isActive = true + } else { + widthConstraint?.isActive = false + } + + if size.height.isFinite { + heightConstraint?.constant = size.height + heightConstraint?.isActive = true + } else { + heightConstraint?.isActive = false + } + } + + func setupCenterYOffset(centerYConstraint: NSLayoutConstraint?, + topConstraint: NSLayoutConstraint?, + bottomConstraint: NSLayoutConstraint?) { + + let centerYOffset = centerOffset.vertical + + if centerYOffset.isFinite { + centerYConstraint?.constant = centerYOffset + centerYConstraint?.isActive = true + topConstraint?.isActive = false + bottomConstraint?.isActive = false + + } else { + topConstraint?.constant = insets.top + bottomConstraint?.constant = -insets.bottom + centerYConstraint?.isActive = false + topConstraint?.isActive = true + bottomConstraint?.isActive = true + } + } + + func setupCenterXOffset(centerXConstraint: NSLayoutConstraint?, + leadingConstraint: NSLayoutConstraint?, + trailingConstraint: NSLayoutConstraint?) { + + let centerXOffset = centerOffset.horizontal + + if centerXOffset.isFinite { + centerXConstraint?.constant = centerXOffset + centerXConstraint?.isActive = true + leadingConstraint?.isActive = false + trailingConstraint?.isActive = false + + } else { + leadingConstraint?.constant = insets.left + trailingConstraint?.constant = -insets.right + centerXConstraint?.isActive = false + leadingConstraint?.isActive = true + trailingConstraint?.isActive = true + } + } +} diff --git a/TIUIElements/Sources/Views/ListItemView/BaseListItemAppearance.swift b/TIUIElements/Sources/Views/ListItemView/BaseListItemAppearance.swift new file mode 100644 index 00000000..5e4b843f --- /dev/null +++ b/TIUIElements/Sources/Views/ListItemView/BaseListItemAppearance.swift @@ -0,0 +1,51 @@ +// +// 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 +import UIKit + +open class BaseListItemAppearance: UIView.BaseAppearance { + + public var leadingViewAppearance: LeadingViewAppearance + public var middleViewAppearance: MiddleViewAppearance + public var trailingAppearance: TrailingViewAppearance + + public init(layout: UIView.DefaultWrappedLayout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + leadingViewAppearance: LeadingViewAppearance = .defaultAppearance, + middleViewAppearance: MiddleViewAppearance = .defaultAppearance, + trailingViewAppearance: TrailingViewAppearance = .defaultAppearance) { + + self.leadingViewAppearance = leadingViewAppearance + self.middleViewAppearance = middleViewAppearance + self.trailingAppearance = trailingViewAppearance + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } +} diff --git a/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift b/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift new file mode 100644 index 00000000..4d2003b3 --- /dev/null +++ b/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift @@ -0,0 +1,258 @@ +// +// 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 +import UIKit + +open class BaseListItemView: BaseInitializableView { + + public let leadingView = LeadingView() + public let middleView = MiddleView() + public let trailingView = TrailingView() + + // MARK: - Constraints + + public var leadingViewLeadingToSuperviewConstraint: NSLayoutConstraint? + public var leadingViewTopConstraint: NSLayoutConstraint? + public var leadingViewBottomConstraint: NSLayoutConstraint? + public var leadingViewCenterYConstraint: NSLayoutConstraint? + public var leadingViewWidthConstraint: NSLayoutConstraint? + public var leadingViewHeightConstraint: NSLayoutConstraint? + + public var middleViewLeadingToSuperViewConstraint: NSLayoutConstraint? + public var middleViewLeadingConstraint: NSLayoutConstraint? + public var middleViewTopConstraint: NSLayoutConstraint? + public var middleViewTrailingToSuperViewConstraint: NSLayoutConstraint? + public var middleViewBottomConstraint: NSLayoutConstraint? + public var middleViewCenterYConstraint: NSLayoutConstraint? + public var middleViewWidthConstraint: NSLayoutConstraint? + public var middleViewHeightConstraint: NSLayoutConstraint? + + public var trailingViewLeadingToMiddleViewConstraint: NSLayoutConstraint? + public var trailingViewTopConstraint: NSLayoutConstraint? + public var trailingViewTrailingToSuperviewConstraint: NSLayoutConstraint? + public var trailingViewBottomConstraint: NSLayoutConstraint? + public var trailingViewCenterYConstraint: NSLayoutConstraint? + public var trailingViewWidthConstraint: NSLayoutConstraint? + public var trailingViewHeightConstraint: NSLayoutConstraint? + + // MARK: - Public Properties + + public var leadingViewConstraints: [NSLayoutConstraint?] { + [ + leadingViewLeadingToSuperviewConstraint, + leadingViewTopConstraint, + leadingViewBottomConstraint, + leadingViewCenterYConstraint, + leadingViewHeightConstraint, + leadingViewWidthConstraint + ] + } + + public var trailingViewConstraints: [NSLayoutConstraint?] { + [ + trailingViewLeadingToMiddleViewConstraint, + trailingViewTopConstraint, + trailingViewBottomConstraint, + trailingViewTrailingToSuperviewConstraint, + trailingViewCenterYConstraint, + trailingViewHeightConstraint, + trailingViewWidthConstraint, + ] + } + + open var isLeadingViewHidden: Bool { + leadingView.isHidden + } + + open var isTrailingViewHidden: Bool { + trailingView.isHidden + } + + // MARK: - BaseInitializableView + + open override func addViews() { + super.addViews() + + addSubviews(leadingView, middleView, trailingView) + } + + open override func configureLayout() { + super.configureLayout() + + [leadingView, middleView, trailingView] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + leadingViewLeadingToSuperviewConstraint = leadingView.leadingAnchor.constraint(equalTo: leadingAnchor) + leadingViewTopConstraint = leadingView.topAnchor.constraint(equalTo: topAnchor) + leadingViewBottomConstraint = leadingView.bottomAnchor.constraint(equalTo: bottomAnchor) + leadingViewCenterYConstraint = leadingView.centerYAnchor.constraint(equalTo: centerYAnchor) + leadingViewWidthConstraint = leadingView.widthAnchor.constraint(equalToConstant: .zero) + leadingViewHeightConstraint = leadingView.heightAnchor.constraint(equalToConstant: .zero) + + middleViewLeadingToSuperViewConstraint = middleView.leadingAnchor.constraint(equalTo: leadingAnchor) + middleViewLeadingConstraint = middleView.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor) + middleViewCenterYConstraint = middleView.centerYAnchor.constraint(equalTo: centerYAnchor) + middleViewTopConstraint = middleView.topAnchor.constraint(equalTo: topAnchor) + middleViewTrailingToSuperViewConstraint = middleView.trailingAnchor.constraint(equalTo: trailingAnchor) + middleViewBottomConstraint = middleView.bottomAnchor.constraint(equalTo: bottomAnchor) + middleViewWidthConstraint = middleView.widthAnchor.constraint(equalToConstant: .zero) + middleViewHeightConstraint = middleView.heightAnchor.constraint(equalToConstant: .zero) + + trailingViewLeadingToMiddleViewConstraint = trailingView.leadingAnchor.constraint(equalTo: middleView.trailingAnchor) + trailingViewTopConstraint = trailingView.topAnchor.constraint(equalTo: topAnchor) + trailingViewTrailingToSuperviewConstraint = trailingView.trailingAnchor.constraint(equalTo: trailingAnchor) + trailingViewBottomConstraint = trailingView.bottomAnchor.constraint(equalTo: bottomAnchor) + trailingViewCenterYConstraint = trailingView.centerYAnchor.constraint(equalTo: centerYAnchor) + trailingViewWidthConstraint = trailingView.widthAnchor.constraint(equalToConstant: .zero) + trailingViewHeightConstraint = trailingView.heightAnchor.constraint(equalToConstant: .zero) + + NSLayoutConstraint.activate([ + leadingViewLeadingToSuperviewConstraint, + leadingViewTopConstraint, + leadingViewBottomConstraint, + + middleViewLeadingConstraint, + + trailingViewTopConstraint, + trailingViewBottomConstraint, + trailingViewLeadingToMiddleViewConstraint, + trailingViewTrailingToSuperviewConstraint, + ].compactMap { $0 }) + + NSLayoutConstraint.deactivate([ + leadingViewCenterYConstraint, + leadingViewWidthConstraint, + leadingViewHeightConstraint, + + middleViewLeadingToSuperViewConstraint, + middleViewTopConstraint, + middleViewTrailingToSuperViewConstraint, + middleViewBottomConstraint, + middleViewCenterYConstraint, + middleViewWidthConstraint, + middleViewHeightConstraint, + + trailingViewCenterYConstraint, + trailingViewHeightConstraint, + trailingViewWidthConstraint, + ].compactMap { $0 }) + } + + // MARK: - Public methods + + public func configureBaseListItem(appearance: BaseListItemAppearance) { + + configureUIView(appearance: appearance) + + updateLeadingViewLayout(leadingViewLayout: appearance.leadingViewAppearance.layout, + middleViewLayout: appearance.middleViewAppearance.layout) + + updateMiddleViewLayout(containerLayout: appearance.layout, + middleViewLayout: appearance.middleViewAppearance.layout) + + updateTrailingViewLayout(trailingViewLayout: appearance.trailingAppearance.layout, + middleViewLayout: appearance.middleViewAppearance.layout) + } + + // MARK: - Private methdos + + private func updateLeadingViewLayout(leadingViewLayout: WrappedViewLayout, + middleViewLayout: WrappedViewLayout) { + + let leadingToSuperviewContraint: NSLayoutConstraint? + let leadingViewLeftInset: CGFloat + + if isLeadingViewHidden { + NSLayoutConstraint.deactivate(leadingViewConstraints.compactMap { $0 }) + middleViewLeadingToSuperViewConstraint?.isActive = true + + leadingToSuperviewContraint = middleViewLeadingToSuperViewConstraint + leadingViewLeftInset = middleViewLayout.insets.left + + } else { + middleViewLeadingConstraint?.constant = leadingViewLayout.insets.right + middleViewLayout.insets.left + leadingViewLeadingToSuperviewConstraint?.isActive = true + middleViewLeadingConstraint?.isActive = true + middleViewLeadingToSuperViewConstraint?.isActive = false + + leadingViewLayout.setupCenterYOffset(centerYConstraint: leadingViewCenterYConstraint, + topConstraint: leadingViewTopConstraint, + bottomConstraint: leadingViewBottomConstraint) + + leadingViewLayout.setupSize(widthConstraint: leadingViewWidthConstraint, + heightConstraint: leadingViewHeightConstraint) + + leadingToSuperviewContraint = leadingViewLeadingToSuperviewConstraint + leadingViewLeftInset = leadingViewLayout.insets.left + } + + leadingToSuperviewContraint?.constant = leadingViewLeftInset + } + + private func updateMiddleViewLayout(containerLayout: ViewLayout, + middleViewLayout: WrappedViewLayout) { + + middleViewLayout.setupCenterYOffset(centerYConstraint: middleViewCenterYConstraint, + topConstraint: middleViewTopConstraint, + bottomConstraint: middleViewBottomConstraint) + + middleViewLayout.setupSize(widthConstraint: middleViewWidthConstraint, + heightConstraint: middleViewHeightConstraint) + } + + private func updateTrailingViewLayout(trailingViewLayout: WrappedViewLayout, + middleViewLayout: WrappedViewLayout) { + + let trailingToSuperviewConstraint: NSLayoutConstraint? + let trailingViewRightInset: CGFloat + + if isTrailingViewHidden { + NSLayoutConstraint.deactivate(trailingViewConstraints.compactMap { $0 }) + middleViewTrailingToSuperViewConstraint?.isActive = true + + trailingToSuperviewConstraint = middleViewTrailingToSuperViewConstraint + trailingViewRightInset = middleViewLayout.insets.right + + } else { + trailingViewLeadingToMiddleViewConstraint?.constant = middleViewLayout.insets.right + trailingViewLayout.insets.left + trailingViewLeadingToMiddleViewConstraint?.isActive = true + middleViewTrailingToSuperViewConstraint?.isActive = false + + trailingViewLayout.setupCenterYOffset(centerYConstraint: trailingViewCenterYConstraint, + topConstraint: trailingViewTopConstraint, + bottomConstraint: trailingViewBottomConstraint) + + trailingViewLayout.setupSize(widthConstraint: trailingViewWidthConstraint, + heightConstraint: trailingViewWidthConstraint) + + trailingToSuperviewConstraint = trailingViewTrailingToSuperviewConstraint + trailingViewRightInset = trailingViewLayout.insets.right + } + + trailingToSuperviewConstraint?.constant = -trailingViewRightInset + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Animation/BaseSkeletonsAnimationConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Animation/BaseSkeletonsAnimationConfiguration.swift new file mode 100644 index 00000000..782617dc --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Animation/BaseSkeletonsAnimationConfiguration.swift @@ -0,0 +1,35 @@ +// +// 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 QuartzCore + +open class BaseSkeletonsAnimationConfiguration { + + public var duration: CFTimeInterval + public var timingFunction: CAMediaTimingFunction? + + public init(duration: CFTimeInterval = 1, timingFunction: CAMediaTimingFunction? = nil) { + self.duration = duration + self.timingFunction = timingFunction + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift new file mode 100644 index 00000000..5ecbc21e --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift @@ -0,0 +1,37 @@ +// +// 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 QuartzCore + +open class DirectionalSkeletonsAnimationConfiguration: BaseSkeletonsAnimationConfiguration { + + public var direction: SkeletonsAnimationDirection + + public init(direction: SkeletonsAnimationDirection = .leftToRight, + duration: CFTimeInterval = 1.5, + timingFunction: CAMediaTimingFunction? = nil) { + + self.direction = direction + + super.init(duration: duration, timingFunction: timingFunction) + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationBuilder.swift b/TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationBuilder.swift new file mode 100644 index 00000000..45210858 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationBuilder.swift @@ -0,0 +1,45 @@ +// +// 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 QuartzCore + +open class SkeletonsAnimationBuilder { + + public static func createDirectionalGradientAnimation(_ conf: DirectionalSkeletonsAnimationConfiguration) -> CAAnimationGroup { + + let startPointAnimation = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.startPoint)) + startPointAnimation.fromValue = conf.direction.startPoint.from + startPointAnimation.toValue = conf.direction.startPoint.to + + let endPointAnimation = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.endPoint)) + endPointAnimation.fromValue = conf.direction.endPoint.from + endPointAnimation.toValue = conf.direction.endPoint.to + + let animationGroup = CAAnimationGroup() + animationGroup.timingFunction = conf.timingFunction + animationGroup.duration = conf.duration + animationGroup.animations = [startPointAnimation, endPointAnimation] + animationGroup.repeatCount = .infinity + + return animationGroup + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationDirection.swift b/TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationDirection.swift new file mode 100644 index 00000000..de09b9a1 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationDirection.swift @@ -0,0 +1,92 @@ +// +// 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 struct CoreGraphics.CGPoint + +typealias GradientAnimationAnchorPoints = (from: CGPoint, to: CGPoint) + +public enum SkeletonsAnimationDirection { + case leftToRight + case rightToLeft + case topToBottom + case bottomToTop + case topLeftToBottomRight + case topRightToBottomLeft + case bottomLeftToTopRight + case bottomRightToTopLeft + + var startPoint: GradientAnimationAnchorPoints { + switch self { + case .leftToRight: + return (from: CGPoint(x: -1, y: 0.5), to: CGPoint(x: 1, y: 0.5)) + + case .rightToLeft: + return (from: Self.leftToRight.startPoint.to, to: Self.leftToRight.startPoint.from) + + case .topToBottom: + return (from: CGPoint(x: 0.5, y: -1), to: CGPoint(x: 0.5, y: 1)) + + case .bottomToTop: + return (from: Self.topToBottom.startPoint.to, to: Self.topToBottom.startPoint.from) + + case .topLeftToBottomRight: + return (from: CGPoint(x: -1, y: -1), to: CGPoint(x: 1, y: 1)) + + case .topRightToBottomLeft: + return (from: Self.bottomLeftToTopRight.startPoint.to, to: Self.bottomLeftToTopRight.startPoint.from) + + case .bottomLeftToTopRight: + return (from: CGPoint(x: -1, y: 2), to: CGPoint(x: 1, y: 0)) + + case .bottomRightToTopLeft: + return (from: Self.topLeftToBottomRight.startPoint.to, to: Self.topLeftToBottomRight.startPoint.from) + } + } + + var endPoint: GradientAnimationAnchorPoints { + switch self { + case .leftToRight: + return (from: CGPoint(x: 0, y: 0.5), to: CGPoint(x: 2, y: 0.5)) + + case .rightToLeft: + return (from: Self.leftToRight.endPoint.to, to: Self.leftToRight.endPoint.from) + + case .topToBottom: + return (from: CGPoint(x: 0.5, y: 0), to: CGPoint(x: 0.5, y: 2)) + + case .bottomToTop: + return (from: Self.topToBottom.endPoint.to, to: Self.topToBottom.endPoint.from) + + case .topLeftToBottomRight: + return (from: CGPoint(x: 0, y: 0), to: CGPoint(x: 2, y: 2)) + + case .topRightToBottomLeft: + return (from: Self.bottomLeftToTopRight.endPoint.to, to: Self.bottomLeftToTopRight.endPoint.from) + + case .bottomLeftToTopRight: + return (from: CGPoint(x: 0, y: 1), to: CGPoint(x: 2, y: -1)) + + case .bottomRightToTopLeft: + return (from: Self.topLeftToBottomRight.endPoint.to, to: Self.topLeftToBottomRight.endPoint.from) + } + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift new file mode 100644 index 00000000..5ad77c0e --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift @@ -0,0 +1,58 @@ +// +// 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 BaseViewSkeletonsConfiguration { + + public enum Shape { + case rectangle(cornerRadius: CGFloat) + case circle + case custom(CGPath) + } + + public var padding: UIEdgeInsets + public var shape: Shape + + public init(padding: UIEdgeInsets = .edges(5), shape: Shape = .rectangle(cornerRadius: .zero)) { + self.shape = shape + self.padding = padding + } + + open func drawPath(rect: CGRect) -> CGPath { + switch shape { + case let .custom(path): + return path + + case let .rectangle(cornerRadius: cornerRadius): + let path = UIBezierPath(roundedRect: rect.reduceSize(byPadding: padding), cornerRadius: cornerRadius) + return path.cgPath + + case .circle: + return CGPath(ellipseIn: rect.reduceSize(byPadding: padding), transform: nil) + } + } + + open func applyPadding(viewFrame: CGRect) -> CGRect { + viewFrame.with(padding: padding) + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/ContainerViewSkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/ContainerViewSkeletonsConfiguration.swift new file mode 100644 index 00000000..98fc81ea --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/ContainerViewSkeletonsConfiguration.swift @@ -0,0 +1,37 @@ +// +// 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 ContainerViewSkeletonsConfiguration: BaseViewSkeletonsConfiguration { + + public var borderWidth: CGFloat + + public init(borderWidth: CGFloat = .zero, + padding: UIEdgeInsets = .zero, + shape: Shape = .rectangle(cornerRadius: .zero)) { + + self.borderWidth = borderWidth + + super.init(padding: padding, shape: shape) + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift new file mode 100644 index 00000000..c46c8e62 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift @@ -0,0 +1,100 @@ +// +// 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 UIKit + +open class SkeletonsConfiguration { + + public var viewConfiguration: BaseViewSkeletonsConfiguration + public var containerViewConfiguration: ContainerViewSkeletonsConfiguration + public var labelConfiguration: TextSkeletonsConfiguration + public var imageViewConfiguration: BaseViewSkeletonsConfiguration + public var animation: Closure? + + public var baseSkeletonBackgroundColor: CGColor? + public var skeletonsBackgroundColor: CGColor + public var skeletonsMovingColor: CGColor + + public weak var configurationDelegate: SkeletonsConfigurationDelegate? + + open var isContainersHidden: Bool { + containerViewConfiguration.borderWidth == .zero + } + + // MARK: - Init + + public init(viewConfiguration: BaseViewSkeletonsConfiguration = .init(), + containerViewConfiguration: ContainerViewSkeletonsConfiguration = .init(), + labelConfiguration: TextSkeletonsConfiguration = .init(), + imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(shape: .circle), + animation: Closure? = nil, + baseSkeletonBackgroundColor: UIColor? = nil, + skeletonsBackgroundColor: UIColor = .lightGray.withAlphaComponent(0.7), + configurationDelegate: SkeletonsConfigurationDelegate? = nil) { + + self.viewConfiguration = viewConfiguration + self.containerViewConfiguration = containerViewConfiguration + self.labelConfiguration = labelConfiguration + self.imageViewConfiguration = imageViewConfiguration + self.animation = animation + self.baseSkeletonBackgroundColor = baseSkeletonBackgroundColor?.cgColor + self.skeletonsBackgroundColor = skeletonsBackgroundColor.cgColor + self.skeletonsMovingColor = skeletonsBackgroundColor.withAlphaComponent(0.2).cgColor + self.configurationDelegate = configurationDelegate + } + + // MARK: - Open methods + + open func createSkeletonLayer(for baseView: UIView) -> SkeletonLayer { + SkeletonLayer(config: self, baseView: baseView) + } + + open func configureAppearance(layer: SkeletonLayer) { + layer.fillColor = skeletonsBackgroundColor + } + + open func configureBaseViewAppearance(layer: SkeletonLayer, view: UIView) { + layer.fillColor = baseSkeletonBackgroundColor ?? view.backgroundColor?.cgColor + } + + open func configureContainerAppearance(layer: SkeletonLayer) { + layer.fillColor = UIColor.clear.cgColor + + if !isContainersHidden { + layer.borderColor = skeletonsBackgroundColor + layer.borderWidth = containerViewConfiguration.borderWidth + + if case let .rectangle(cornerRadius: radius) = containerViewConfiguration.shape { + layer.cornerRadius = radius + } + } + } + + open func configureAppearance(gradientLayer: CAGradientLayer) { + gradientLayer.colors = [ + skeletonsBackgroundColor, + skeletonsMovingColor, + skeletonsBackgroundColor, + ] + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/TextSkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/TextSkeletonsConfiguration.swift new file mode 100644 index 00000000..ef68b06a --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/TextSkeletonsConfiguration.swift @@ -0,0 +1,160 @@ +// +// 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 UIKit + +open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration { + + private enum Constants { + static var defaultNumberOfLines: Int { + 3 + } + } + + private var isMultiline = false + private var labelNumberOfLines: Int = .zero + private var labelHeight: CGFloat = .zero + private var font: UIFont? + + public var numberOfLines: Int? + public var lineHeight: Closure? + public var lineSpacing: Closure? + + public init(numberOfLines: Int? = nil, + lineHeight: Closure? = nil, + lineSpacing: Closure? = nil, + padding: UIEdgeInsets = .edges(5), + shape: Shape = .rectangle(cornerRadius: .zero)) { + + self.numberOfLines = numberOfLines + self.lineHeight = lineHeight + self.lineSpacing = lineSpacing + + super.init(padding: padding, shape: shape) + } + + open override func drawPath(rect: CGRect) -> CGPath { + /* + SkeletonLayer + |-------------------------| + ||-----------------------|| - first line CGRect(0, 0, rect.width, lineHeight) + | | - spacing + ||-----------------------|| - second line CGRect(0, lineHeight + spacing, rect.width, lineHeight) + | | - spacing + ||-----------------------|| - third line CGRect(0, (lineHeight + spacing) * 2, rect.width, lineHeight) + |-------------------------| + */ + let path = UIBezierPath() + let numberOfLines = getNumberOfLines() + let spacing = getLineSpacing() + let lineHeight = getLineHeight() + var cornerRadius = CGFloat.zero + + if case let .rectangle(cornerRadius: radius) = shape { + cornerRadius = radius / 2 + } + + for lineNumber in 0.. CGPath { + if case let .custom(path) = shape { + return path + } + + isMultiline = label.isMultiline + font = label.font + labelNumberOfLines = label.numberOfLines + labelHeight = label.bounds.height + + return drawPath(rect: label.bounds.reduceSize(byPadding: padding)) + } + + open func configureTextViewPath(textView: UITextView) -> CGPath { + if case let .custom(path) = shape { + return path + } + + isMultiline = textView.isMultiline + font = textView.font + labelNumberOfLines = textView.textContainer.maximumNumberOfLines + labelHeight = textView.bounds.height + + return drawPath(rect: textView.bounds.reduceSize(byPadding: padding)) + } + + // MARK: - Private methods + + private func getLineHeight() -> CGFloat { + if let lineHeight = lineHeight?(font) { + return lineHeight + } + + // By default height of the line is equal to 75% of font's size + return (font?.pointSize ?? 1) * 0.75 + } + + private func getLineSpacing() -> CGFloat { + if let lineSpacing = lineSpacing?(font) { + return lineSpacing + } + + return font?.xHeight ?? .zero + } + + private func getNumberOfLines() -> Int { + guard isMultiline else { + return 1 + } + + if let numberOfLines = numberOfLines { + return numberOfLines + } + + return labelNumberOfLines == .zero + ? Constants.defaultNumberOfLines + : labelNumberOfLines + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Helpers/CALayer+Skeletons.swift b/TIUIElements/Sources/Views/Skeletons/Helpers/CALayer+Skeletons.swift new file mode 100644 index 00000000..82877f21 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Helpers/CALayer+Skeletons.swift @@ -0,0 +1,29 @@ +// +// 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 QuartzCore + +extension CALayer { + public var skeletonLayers: [SkeletonLayer] { + (sublayers ?? []).compactMap { $0 as? SkeletonLayer } + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Helpers/CGRect+Padding.swift b/TIUIElements/Sources/Views/Skeletons/Helpers/CGRect+Padding.swift new file mode 100644 index 00000000..98702362 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Helpers/CGRect+Padding.swift @@ -0,0 +1,40 @@ +// +// 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 + +extension CGRect { + func with(padding: UIEdgeInsets) -> CGRect { + CGRect(x: minX + padding.left, + y: minY + padding.top, + width: width, + height: height) + } + + func reduceSize(byPadding padding: UIEdgeInsets) -> CGRect { + let reducedWidth = width - padding.left - padding.right + let reducedHeight = height - padding.top - padding.bottom + let reducedSize = CGSize(width: reducedWidth, height: reducedHeight) + + return CGRect(origin: origin, size: reducedSize) + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Helpers/UIView+Skeletons.swift b/TIUIElements/Sources/Views/Skeletons/Helpers/UIView+Skeletons.swift new file mode 100644 index 00000000..fc8c98a5 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Helpers/UIView+Skeletons.swift @@ -0,0 +1,121 @@ +// +// 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 +import UIKit + +// MARK: - UITextView + is multiline + +extension UITextView { + var isMultiline: Bool { + isMultiline(text: text, + attributedText: attributedText, + font: font, + textAlignment: textAlignment) + } +} + +// MARK: - UILabel + is multiline + +extension UILabel { + var isMultiline: Bool { + isMultiline(text: text, + attributedText: attributedText, + font: font, + textAlignment: textAlignment) + } +} + +// MARK: - UIView + Skeleton helpers + +extension UIView { + public var skeletonableViews: [UIView] { + if let skeletonableView = self as? Skeletonable { + return skeletonableView.viewsToSkeletons + } + + return subviews + } + + var isSkeletonsContainer: Bool { + if let skeletonableView = self as? Skeletonable { + return !skeletonableView.viewsToSkeletons.isEmpty + } + + return !subviews.isEmpty + } + + var viewType: SkeletonLayer.ViewType { + if let labelView = self as? UILabel { + return .label(labelView) + } + + if let textView = self as? UITextView { + return .textView(textView) + } + + if let imageView = self as? UIImageView { + return .imageView(imageView) + } + + if self.isSkeletonsContainer { + return .parentView(self) + } + + return .leafView(self) + } + + fileprivate func isMultiline(text: String?, + attributedText: NSAttributedString?, + font: UIFont?, + textAlignment: NSTextAlignment) -> Bool { + let finalText: String + let finalFont: UIFont + + if let attributedText = attributedText, let maxFont = attributedText.getMaxFont() { + finalText = attributedText.string + finalFont = maxFont + + } else if let text = text, let font = font { + finalText = text + finalFont = font + + } else { + return false + } + + let textAttributes = BaseTextAttributes(font: finalFont, color: .black, alignment: textAlignment, isMultiline: true) + let labelTextSize = textAttributes.size(of: finalText, with: .zero) + + return labelTextSize.width > bounds.width + } +} + +// MARK: - NSAttributedString helper extension + +extension NSAttributedString { + func getMaxFont() -> UIFont? { + (0.. Bool { + view.layer.skeletonLayers.isEmpty + } +} + +// MARK: - UIView + SkeletonsPresenter + +extension SkeletonsPresenter where Self: UIView { + public var skeletonsHolder: UIView { + self + } +} + +// MARK: - UIViewController + SkeletonsPresenter + +extension SkeletonsPresenter where Self: UIViewController { + public var skeletonsHolder: UIView { + view + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift new file mode 100644 index 00000000..758765ff --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift @@ -0,0 +1,206 @@ +// +// 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 SkeletonLayer: CAShapeLayer { + + private enum Constants { + static var animationKeyPath: String { + "skeletonAnimation" + } + } + + public enum ViewType { + case skeletonsHolderView(UIView) + case parentView(UIView) + case imageView(UIImageView) + case textView(UITextView) + case label(UILabel) + case leafView(UIView) + + public var view: UIView { + switch self { + case let .imageView(imageView): + return imageView + + case let .parentView(containerView): + return containerView + + case let .label(labelView): + return labelView + + case let .textView(textView): + return textView + + case let .leafView(view): + return view + + case let .skeletonsHolderView(view): + return view + } + } + } + + private var animationLayer = CAGradientLayer() + private var viewBoundsObservation: NSKeyValueObservation? + private var applicationStateObservation: NSObjectProtocol? + + public var configuration: SkeletonsConfiguration + public var isSkeletonsHolder: Bool = false + public weak var baseView: UIView? + + public var isAnimating: Bool { + animationLayer.animation(forKey: Constants.animationKeyPath) != nil + } + + // MARK: - Init + + // For debug purposes in Lookin or other programs for view hierarchy inspections + public override init(layer: Any) { + self.configuration = .init() + + super.init(layer: layer) + } + + public init(config: SkeletonsConfiguration, baseView: UIView) { + self.configuration = config + self.baseView = baseView + + super.init() + } + + public required init?(coder: NSCoder) { + self.configuration = .init() + + super.init(coder: coder) + } + + // MARK: - Open methods + + open func bind(to viewType: ViewType) { + configureAppearance(viewType) + updateGeometry(viewType: viewType) + + viewBoundsObservation = viewType.view.observe(\.frame, options: [.new]) { [weak self] view, _ in + self?.updateGeometry(viewType: view.viewType) + } + + if let _ = configuration.animation?(self) { + applicationStateObservation = NotificationCenter.default + .addObserver(forName: UIApplication.willEnterForegroundNotification, + object: nil, + queue: .main) { [weak self] _ in + self?.stopAnimation() + self?.startAnimation() + } + } + + configuration.configurationDelegate?.layerDidConfigured(self, forViewType: viewType) + } + + open func remove(from view: UIView) { + stopAnimation() + removeFromSuperlayer() + viewBoundsObservation?.invalidate() + + if let observation = applicationStateObservation { + NotificationCenter.default.removeObserver(observation) + } + } + + open func startAnimation() { + guard !isAnimating, + !isSkeletonsHolder, + let animation = configuration.animation?(self) else { + return + } + + animationLayer.add(animation, forKey: Constants.animationKeyPath) + mask = animationLayer + } + + open func stopAnimation() { + animationLayer.removeAllAnimations() + mask?.removeFromSuperlayer() + } + + // MARK: - Private methods + + private func configureAppearance(_ type: ViewType) { + switch type { + case .parentView(_): + configuration.configureContainerAppearance(layer: self) + + case .skeletonsHolderView(_): + isSkeletonsHolder = true + configuration.configureBaseViewAppearance(layer: self, view: type.view) + + default: + configuration.configureAppearance(layer: self) + } + + configuration.configureAppearance(gradientLayer: animationLayer) + } + + private func updateGeometry(viewType: ViewType) { + let rect = viewType.view.convert(viewType.view.bounds, to: baseView) + + switch viewType { + case let .textView(textView): + path = configuration.labelConfiguration.configureTextViewPath(textView: textView) + + let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size) + frame = configuration.labelConfiguration.applyPadding(viewFrame: viewFrame) + + case let .label(label): + path = configuration.labelConfiguration.configureLabelPath(label: label) + + let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size) + frame = configuration.labelConfiguration.applyPadding(viewFrame: viewFrame) + + case .imageView(_): + path = configuration.imageViewConfiguration.drawPath(rect: viewType.view.bounds) + + let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size) + frame = configuration.imageViewConfiguration.applyPadding(viewFrame: viewFrame) + + case .parentView(_): + path = configuration.containerViewConfiguration.drawPath(rect: viewType.view.bounds) + + let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size) + frame = configuration.containerViewConfiguration.applyPadding(viewFrame: viewFrame) + + case .leafView(_): + path = configuration.viewConfiguration.drawPath(rect: viewType.view.bounds) + + let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size) + frame = configuration.viewConfiguration.applyPadding(viewFrame: viewFrame) + + default: + path = UIBezierPath(roundedRect: rect, cornerRadius: 20).cgPath + frame = rect + } + + animationLayer.frame = bounds + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/UIView+PresentingSkeletons.swift b/TIUIElements/Sources/Views/Skeletons/UIView+PresentingSkeletons.swift new file mode 100644 index 00000000..47f49706 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/UIView+PresentingSkeletons.swift @@ -0,0 +1,113 @@ +// +// 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 UIKit + +extension UIView { + + // MARK: - Public methods + + /// Shows skeletons on the view + /// + /// - Parameters: + /// - viewsToSkeletons: views that will be converted to skeletons. If nil was passed subviews will be converted to skeletons + /// - config: configuration of the skeletons' layers + public func showSkeletons(viewsToSkeletons: [UIView]?, + _ config: SkeletonsConfiguration) { + + let viewsToSkeletons = viewsToSkeletons ?? skeletonableViews + isUserInteractionEnabled = false + + configureBaseLayer(withConfiguration: config) + + viewsToSkeletons + .flatMap { view in + getSkeletonLayer(forView: view, withConfiguration: config) + } + .map { layer in + layer.startAnimation() + + return layer + } + .insert(onto: self) + } + + public func hideSkeletons() { + isUserInteractionEnabled = true + + layer.skeletonLayers + .forEach { $0.remove(from: self) } + } + + public func startAnimation() { + layer.skeletonLayers + .forEach { $0.startAnimation() } + } + + public func stopAnimation() { + layer.skeletonLayers + .forEach { $0.stopAnimation() } + } + + // MARK: - Private methods + + private func getSkeletonLayer(forView view: UIView, + withConfiguration conf: SkeletonsConfiguration, + forceNoContainers: Bool = false) -> [SkeletonLayer] { + + let skeletonLayer = conf.createSkeletonLayer(for: self) + var subviewSkeletonLayers = [SkeletonLayer]() + + if view.isSkeletonsContainer { + if !conf.isContainersHidden, !forceNoContainers { + skeletonLayer.bind(to: .parentView(view)) + } + + subviewSkeletonLayers = view.skeletonableViews + .map { getSkeletonLayer(forView: $0, withConfiguration: conf, forceNoContainers: true) } + .flatMap { $0 } + + } else { + skeletonLayer.bind(to: view.viewType) + } + + return [skeletonLayer] + subviewSkeletonLayers + } + + private func configureBaseLayer(withConfiguration conf: SkeletonsConfiguration) { + let skeletonLayer = conf.createSkeletonLayer(for: self) + + skeletonLayer.bind(to: .skeletonsHolderView(self)) + layer.insertSublayer(skeletonLayer, at: .max) + } +} + +// MARK: - Helper extension + +extension Array where Element: CALayer { + public func insert(onto view: UIView, at index: UInt32 = .max) { + self.forEach { subLayer in + view.layer.insertSublayer(subLayer, at: index) + } + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/UIViewController+PresentingSkeletons.swift b/TIUIElements/Sources/Views/Skeletons/UIViewController+PresentingSkeletons.swift new file mode 100644 index 00000000..5da4d039 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/UIViewController+PresentingSkeletons.swift @@ -0,0 +1,49 @@ +// +// 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 + +extension UIViewController { + + /// Shows skeletons + /// + /// - Parameters: + /// - viewsToSkeletons: views that will be converted to skeletons. If nil was passed subviews of the view will be converted to skeletons + /// - config: configuration of the skeletons' layers + public func showSkeletons(viewsToSkeletons: [UIView]?, + _ config: SkeletonsConfiguration) { + + view.showSkeletons(viewsToSkeletons: viewsToSkeletons, config) + } + + public func hideSkeletons() { + view.hideSkeletons() + } + + public func startAnimation() { + view.startAnimation() + } + + public func stopAnimation() { + view.stopAnimation() + } +} diff --git a/TIUIElements/Sources/Views/StatefulButton/RoundedStatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/RoundedStatefulButton.swift index 9d9cb5e0..893995eb 100644 --- a/TIUIElements/Sources/Views/StatefulButton/RoundedStatefulButton.swift +++ b/TIUIElements/Sources/Views/StatefulButton/RoundedStatefulButton.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +import TIUIKitCore import UIKit open class RoundedStatefulButton: StatefulButton { diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift index 8d0fcd6f..2c3abe2d 100644 --- a/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton.swift @@ -42,6 +42,8 @@ open class StatefulButton: UIButton { } } + public typealias Appearance = UIButton.BaseAppearance + public typealias StateAppearance = [State: Appearance] public typealias StateEventPropagations = [State: Bool] private var activityIndicator: ActivityIndicator? { @@ -73,24 +75,22 @@ open class StatefulButton: UIButton { // MARK: - Background - private var backgroundColors: StateColors = [:] { + private var stateAppearance: StateAppearance = [:] { didSet { - updateBackgroundColor() + updateAppearance() } } - public func set(backgroundColors: StateColors) { - backgroundColors.forEach { setBackgroundColor($1, for: $0) } + public func set(appearance: StateAppearance) { + appearance.forEach { setAppearance($1, for: $0) } } - public func setBackgroundColor(_ color: UIColor?, for state: State) { - backgroundColors[state] = color + public func setAppearance(_ appearance: Appearance, for state: State) { + stateAppearance[state] = appearance } - public func backgroundColor(for state: State) -> UIColor? { - // Value of optional type 'UIColor??' must be unwrapped to a value of type 'UIColor?' - // 🤷 Swift 5.3 (Xcode 12.2) - backgroundColors[state] ?? nil //swiftlint:disable:this redundant_nil_coalescing + public func appearance(for state: State) -> Appearance? { + stateAppearance[state] } public func setEventPropagation(_ eventPropagation: Bool, for state: State) { @@ -101,19 +101,19 @@ open class StatefulButton: UIButton { override open var isEnabled: Bool { didSet { - updateBackgroundColor() + updateAppearance() } } override open var isHighlighted: Bool { didSet { - updateBackgroundColor() + updateAppearance() } } open override var isSelected: Bool { didSet { - updateBackgroundColor() + updateAppearance() } } @@ -207,23 +207,23 @@ open class StatefulButton: UIButton { } } - private func updateBackgroundColor() { + private func updateAppearance() { if isEnabled { if isHighlighted { - updateBackgroundColor(to: .highlighted) + updateAppearance(to: .highlighted) } else { - updateBackgroundColor(to: .normal) + updateAppearance(to: .normal) } } else { - updateBackgroundColor(to: .disabled) + updateAppearance(to: .disabled) } } - private func updateBackgroundColor(to state: State) { - if let stateColor = backgroundColor(for: state) { - backgroundColor = stateColor - } else if state != .normal, let normalStateColor = backgroundColor(for: .normal) { - backgroundColor = normalStateColor + private func updateAppearance(to state: State) { + if let appearance = stateAppearance[state] { + configureUIButton(appearance: appearance) + } else if state != .normal, let appearance = stateAppearance[.normal] { + configureUIButton(appearance: appearance) } } } diff --git a/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView+Appearance.swift b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView+Appearance.swift new file mode 100644 index 00000000..dc6906a2 --- /dev/null +++ b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView+Appearance.swift @@ -0,0 +1,53 @@ +// +// 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 +import UIKit + +extension DefaultTitleSubtitleView { + + public final class Appearance: UIView.BaseAppearance, WrappedViewAppearance { + + public static var defaultAppearance: Appearance { + Self() + } + + public var titleAppearance: UILabel.DefaultAppearance + public var subtitleAppearance: UILabel.DefaultAppearance + + public init(layout: Layout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + titleAppearance: UILabel.DefaultAppearance = .defaultAppearance, + subtitleAppearance: UILabel.DefaultAppearance = .defaultAppearance) { + + self.titleAppearance = titleAppearance + self.subtitleAppearance = subtitleAppearance + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } + } +} diff --git a/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView.swift b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView.swift new file mode 100644 index 00000000..4dc0c7e4 --- /dev/null +++ b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleView.swift @@ -0,0 +1,95 @@ +// +// 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 +import UIKit + +public final class DefaultTitleSubtitleView: BaseInitializableView, + ConfigurableView, + AppearanceConfigurable { + + private let titleLabel = UILabel() + private let subtitleLabel = UILabel() + + public var titleLableBottomConstraint: NSLayoutConstraint? + public var spacingConstraint: NSLayoutConstraint? + + // MARK: - InitializableViewProtocol + + public override func addViews() { + super.addViews() + + addSubviews(titleLabel, subtitleLabel) + } + + public override func configureLayout() { + super.configureLayout() + + [titleLabel, subtitleLabel] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + titleLableBottomConstraint = titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + spacingConstraint = subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + titleLabel.topAnchor.constraint(equalTo: topAnchor), + titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + + subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + spacingConstraint, + subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor), + ].compactMap { $0 }) + } + + // MARK: - ConfigurableView + + public func configure(with viewModel: DefaultTitleSubtitleViewModel) { + titleLabel.text = viewModel.title + subtitleLabel.text = viewModel.subtitle + + setSubtitle(hidden: viewModel.isSubtitleHidden) + } + + // MARK: - Public methods + + public func setSubtitle(hidden: Bool) { + subtitleLabel.isHidden = hidden + spacingConstraint?.isActive = !hidden + titleLableBottomConstraint?.isActive = hidden + } + + // MARK: - AppearanceConfigurable + + public func configure(appearance: Appearance) { + configureUIView(appearance: appearance) + titleLabel.configureUILabel(appearance: appearance.titleAppearance) + subtitleLabel.configureUILabel(appearance: appearance.subtitleAppearance) + + configure(layout: appearance.layout) + } + + public func configure(layout: UIView.DefaultSpacedWrappedLayout) { + spacingConstraint?.constant = layout.spacing + } +} diff --git a/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleViewModel.swift b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleViewModel.swift new file mode 100644 index 00000000..ebe482d5 --- /dev/null +++ b/TIUIElements/Sources/Views/TitleSubtitleView/DefaultTitleSubtitleViewModel.swift @@ -0,0 +1,37 @@ +// +// 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 final class DefaultTitleSubtitleViewModel { + public let title: String? + public let subtitle: String? + + public var isSubtitleHidden: Bool { + subtitle == nil + } + + public init(title: String? = nil, + subtitle: String? = nil) { + + self.title = title + self.subtitle = subtitle + } +} diff --git a/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift new file mode 100644 index 00000000..abf70b41 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift @@ -0,0 +1,50 @@ +// +// 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 CollectionTableViewCell: ContainerTableViewCell { + + // MARK: - UIView Overrides + + open override func systemLayoutSizeFitting(_ targetSize: CGSize, + withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, + verticalFittingPriority: UILayoutPriority) -> CGSize { + + let cachedCollectionFrame = wrappedView.frame + wrappedView.frame.size.width = targetSize.width - contentInsets.left - contentInsets.right + let collectionContentHeight = wrappedView.collectionViewLayout.collectionViewContentSize.height + wrappedView.frame = cachedCollectionFrame + return CGSize(width: targetSize.width, + height: collectionContentHeight + contentInsets.top + contentInsets.bottom) + } + + // MARK: - Open methods + + open func createCollectionLayout() -> UICollectionViewLayout { + UICollectionViewFlowLayout() + } + + open override func createView() -> CollectionView { + CollectionView(frame: .zero, collectionViewLayout: createCollectionLayout()) + } +} diff --git a/TIUIElements/Sources/Wrappers/ContainerCollectionViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift similarity index 100% rename from TIUIElements/Sources/Wrappers/ContainerCollectionViewCell.swift rename to TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift diff --git a/TIUIElements/Sources/Wrappers/ContainerTableViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift similarity index 71% rename from TIUIElements/Sources/Wrappers/ContainerTableViewCell.swift rename to TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift index 3892e327..585ed55c 100644 --- a/TIUIElements/Sources/Wrappers/ContainerTableViewCell.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift @@ -53,4 +53,22 @@ open class ContainerTableViewCell: BaseInitializableCell, WrappedV open func createView() -> View { return View() } + + // MARK: - Open methods + + public func configureContainerTableViewCell(appearance: BaseWrappedViewHolderAppearance) { + contentInsets = appearance.subviewAppearance.layout.insets + configureUIView(appearance: appearance) + } +} + +// MARK: - AppearanceConfigurable + +extension ContainerTableViewCell: AppearanceConfigurable where View: AppearanceConfigurable, + View.Appearance: WrappedViewAppearance { + + public func configure(appearance: DefaultWrappedViewHolderAppearance) { + configureContainerTableViewCell(appearance: appearance) + wrappedView.configure(appearance: appearance.subviewAppearance) + } } diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift new file mode 100644 index 00000000..77e07dd8 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift @@ -0,0 +1,74 @@ +// +// 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 +import UIKit + +public final class ContainerView: BaseInitializableView, WrappedViewHolder { + + public var wrappedView = View() + + public var contentInsets: UIEdgeInsets = .zero { + didSet { + contentEdgeConstraints?.update(from: contentInsets) + } + } + + private var contentEdgeConstraints: EdgeConstraints? + + // MARK: - InitializableView + + public override func addViews() { + super.addViews() + + addSubview(wrappedView) + } + + public override func configureLayout() { + super.configureLayout() + + contentEdgeConstraints = configureWrappedViewLayout() + } +} + +// MARK: - ConfigurableView + +extension ContainerView: ConfigurableView where View: ConfigurableView { + + public func configure(with viewModel: View.ViewModelType) { + wrappedView.configure(with: viewModel) + } +} + +// MARK: - AppearanceConfigurable + +extension ContainerView: AppearanceConfigurable where View: AppearanceConfigurable, + View.Appearance: WrappedViewAppearance { + + public typealias Appearance = UIView.DefaultWrappedViewHolderAppearance + + public func configure(appearance: Appearance) { + wrappedView.configure(appearance: appearance.subviewAppearance) + configureUIView(appearance: appearance) + contentInsets = appearance.subviewAppearance.layout.insets + } +} diff --git a/TIUIElements/Sources/Wrappers/ReusableCollectionContainerView.swift b/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift similarity index 100% rename from TIUIElements/Sources/Wrappers/ReusableCollectionContainerView.swift rename to TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift diff --git a/TIUIElements/Sources/Wrappers/Extensions/WrappableView+Containers.swift b/TIUIElements/Sources/Wrappers/Extensions/WrappableView+Containers.swift new file mode 100644 index 00000000..67dffcdc --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Extensions/WrappableView+Containers.swift @@ -0,0 +1,27 @@ +// +// 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 extension WrappableView { + typealias InContainerView = ContainerView + typealias InTableCell = ContainerTableViewCell + typealias InSeparatableTableCell = ContainerSeparatorTableViewCell +} diff --git a/TIUIElements/Sources/Wrappers/WrappedViewHolder+ConfigurableView.swift b/TIUIElements/Sources/Wrappers/Extensions/WrappedViewHolder+ConfigurableView.swift similarity index 100% rename from TIUIElements/Sources/Wrappers/WrappedViewHolder+ConfigurableView.swift rename to TIUIElements/Sources/Wrappers/Extensions/WrappedViewHolder+ConfigurableView.swift diff --git a/TIUIElements/Sources/Wrappers/Protocols/WrappableView.swift b/TIUIElements/Sources/Wrappers/Protocols/WrappableView.swift new file mode 100644 index 00000000..b8489dd7 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Protocols/WrappableView.swift @@ -0,0 +1,27 @@ +// +// 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 + +public protocol WrappableView: UIView {} + +extension UIView: WrappableView {} diff --git a/TIUIElements/Sources/Wrappers/WrappedViewHolder.swift b/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift similarity index 100% rename from TIUIElements/Sources/Wrappers/WrappedViewHolder.swift rename to TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift diff --git a/TIUIElements/TIUIElements.app/.gitignore b/TIUIElements/TIUIElements.app/.gitignore new file mode 100644 index 00000000..b7fe13ce --- /dev/null +++ b/TIUIElements/TIUIElements.app/.gitignore @@ -0,0 +1,4 @@ +# gitignore nef files +**/build/ +**/nef/ +LICENSE \ No newline at end of file diff --git a/TIUIElements/TIUIElements.app/Contents/Info.plist b/TIUIElements/TIUIElements.app/Contents/Info.plist new file mode 100644 index 00000000..831ea97a --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + launcher + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + com.fortysevendeg.nef + CFBundleInfoDictionaryVersion + 6.0 + CFBundleSupportedPlatforms + + MacOSX + + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.14 + NSHumanReadableCopyright + Copyright © 2019 The nef Authors. All rights reserved. + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/.gitignore b/TIUIElements/TIUIElements.app/Contents/MacOS/.gitignore new file mode 100644 index 00000000..18bd1f3b --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/.gitignore @@ -0,0 +1,26 @@ +## 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 diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/Podfile b/TIUIElements/TIUIElements.app/Contents/MacOS/Podfile new file mode 100644 index 00000000..ab33d1bc --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/Podfile @@ -0,0 +1,10 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIUIElements' do + platform :ios, 11.0 + use_frameworks! + + pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec' +end diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..bd63e083 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift @@ -0,0 +1,315 @@ +/*: + # Skeletons API + + При импорте _TIUIElements_ вы можете использовать API для показа скелетонов. + + ## Принцип работы + + При использовании методов показа скелетонов: + 1. происходит скрытие всех subview в иерархии той view, на которой был вызван метод + 2. далее происходит проход по view, которые можно сконвертировать в скелетоны (список либо определяется пользователем, либо конвертация будет происходить автоматически), создается `CALayer` типа `SkeletonLayer`, представляющий конвертируемую view + 3. поверх view с которой начался показ, добавляются все созданные `SkeletonLayer` + + > Таким образом скелетоны не модифицируют размеры view и не изменяют ее положение + + ## Как начать пользоваться + + Базовая настройка для показа скелетонов не требуется. `UIView` и `UIViewController` уже имеют все необходимые методы для работы: + - `showSkeletons(viewsToSkeletons:_:)` : используется для показа скелетонов, если была передана конфигурация анимации, то она запустится автоматически. `viewsToSkeletons` - опциональный массив `UIView`, определяющий, какие вью будут конвертироваться в скелетоны. Если nil, то проход будет осуществляться по списку subview + - `hideSkeletons()` : используется для скрытия скелетонов + - `startAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletons:_:)` то ничего не произойдет) + - `stopAnimation()` : используется для остановки анимации на скелетонах + */ +import TIUIKitCore +import TIUIElements +import UIKit + +class CanShowAndHideSkeletons: BaseInitializableViewController { + + private let imageView = UIImageView(image: UIImage(systemName: "apple.logo")) + private let label = UILabel() + private let button = UIButton(type: .custom) + + override func addViews() { + super.addViews() + + view.addSubviews(imageView, label, button) + } + + override func configureLayout() { + super.configureLayout() + + [imageView, label, button] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + imageView.heightAnchor.constraint(equalToConstant: 40), + imageView.widthAnchor.constraint(equalToConstant: 40), + imageView.centerYAnchor.constraint(equalTo: label.centerYAnchor), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + + label.topAnchor.constraint(equalTo: view.topAnchor), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor), + label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), + label.heightAnchor.constraint(equalToConstant: 60), + + button.leadingAnchor.constraint(equalTo: view.leadingAnchor), + button.topAnchor.constraint(equalTo: label.bottomAnchor), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + override func bindViews() { + super.bindViews() + + button.addTarget(self, action: #selector(toggleSkeletons), for: .touchUpInside) + } + + override func configureAppearance() { + super.configureAppearance() + + label.text = "Hello from SkeletonableViewController" + button.setTitle("show skeletons", for: .normal) + + let textAttributes = BaseTextAttributes(font: .systemFont(ofSize: 25), color: .black, alignment: .natural, isMultiline: false) + + view.configureUIView(appearance: UIView.DefaultAppearance(backgroundColor: .white)) + + label.configureUILabel(appearance: UILabel.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + + button.configureUIButton(appearance: UIButton.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + } + + @objc private func toggleSkeletons() { + // Т.к. передается nil, скелетониться будут все subview (в данном случае view.subview == [button, label, imageView]) + showSkeletons(viewsToSkeletons: nil, .init()) + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in + self?.hideSkeletons() + } + } +} + +/*: + ## Skeletonable + + Если необходимо изменить список конвертируемых в скелетоны view у какой-нибудь из отдельных view в иерархии, можно подписать его под протокол `Skeletonable` + */ +extension UITableViewCell: Skeletonable { + public var viewsToSkeletons: [UIView] { + contentView.subviews + } +} + +/*: + ## SkeletonsPresenter + + Чтобы не приходилось постоянно передавать в методы необходимые параметры для конфигурации можно соответствовать протоколу `SkeletonsPresenter`. Протокол дает возможность определять свойства для конфигурации скелетонов внутри view или viewController, вызывать метод `showSkeletons()` без передачи каких-либо параметров + + Перепишем _CanShowAndHideSkeletons_ под использование протокола + */ + +class CanShowAndHideWithSkeletonsPresenter: CanShowAndHideSkeletons, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + SkeletonsConfiguration(skeletonsBackgroundColor: .gray) + } +} + +let canShowAndHideController = CanShowAndHideWithSkeletonsPresenter() + +//: Skeletons will be shown with custom configuration +canShowAndHideController.showSkeletons() + +/*: + ## Конфигурация внешнего вида + + Для конфигурации скелетонов существует класс `SkeletonsConfiguration` + */ +class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + .init(skeletonsBackgroundColor: .blue) + } +} + +/*: + Возможные опции для настройки: + + - анимация + - цвет + - форма + - отступы + + При этом все view делятся на: + - `UIView` с subviews (контейнеры) + - `UIView` без subviews + - `UILabel` + - `UITextView` + - `UIImageView` + + > Для контейнеров в качестве `borderColor` используется тот же цвет, что и для других скелетонов + + ### Анимация + + `SkeletonsConfiguration` для настройки анимации принимает тип `(SkeletonsLayer) -> CAAnimationGroup`. Это означает, что при необходимости вы можете создать любую необходимую анимацию. Анимация будет применена к каждому скелетону. + + Однако для удобства существует уже определенный класс `SkeletonsAnimationBuilder` со статическим методом `createDirectionalGradientAnimation(_:)` для создания анимаций в одну из сторон: + + ```swift + public enum SkeletonsAnimationDirection { + case leftToRight + case rightToLeft + case topToBottom + case bottomToTop + case topLeftToBottomRight + case topRightToBottomLeft + case bottomLeftToTopRight + case bottomRightToTopLeft + } + ``` + */ +let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .leftToRight, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) + +let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .topToBottom, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) + +/*: + ### Цвет + + За настройку цвета отвечает параметр `skeletonsBackgroundColor`: основной цвет скелетонов, им будт заливаться фон и выделяться _border_ + */ +let confWithRedBackgroundColor = SkeletonsConfiguration(skeletonsBackgroundColor: .red) + +//: Для скрытия view от пользователя скелетонами накладывается специальный CALayer, чей цвет заполнения равен backgroundColor view, с которой были запущены скелетоны. Однако этот цвет можно переопределить переопределить с помощью параметра `baseSkeletonBackgroundColor` +let confWithRedBaseBackgroundColor = SkeletonsConfiguration(baseSkeletonBackgroundColor: .red) + +/*: + ### Форма + + Форму можно настраивать отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, картинки можно сделать круглыми, а лейблы прямоугольные с закругленными краями: + */ +var confWithShape: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} + +//: Для `UILabel` и `UITextView` есть возможность настроить высоту каждой строчки, расстояние между ними и их количество. +var confWithLabelSettings: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(numberOfLines: 3, + lineHeight: { font in + if let font = font { + return font.pointSize + } + return 10 + + }, lineSpacing: { font in + if let font = font { + return font.xHeight + } + return 5 + }) + return .init(labelConfiguration: labelConf) +} + +//: Для контейнеров можно настроить `borderWidth`. Стандартно он равняется 0, а значит контейнеры не будут показываться без дополнительной настройки ширины. +var skeletonsConfiguration: SkeletonsConfiguration { + let containerConf = ContainerViewSkeletonsConfiguration(borderWidth: 4, + shape: .rectangle(cornerRadius: 10)) + + return .init(containerViewConfiguration: containerConf) +} + +/*: + ### Отступы + + Отступы можно настроить отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, для предыдущего примера можно добавить горизонтальный _padding_ для лейбла: + */ +var confWithPadding: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(padding: .horizontal(left: 15), shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} + +/*: + ## Что если нужно больше? + + Если стандартной настройки не хватает, то в конфигуратор можно передать объект, соответствующий протоколу `SkeletonsConfigurationDelegate` через который можно настроить слой скелетона для каждой вью отдельно + + ```swift + public protocol SkeletonsConfigurationDelegate: AnyObject { + func layerDidConfigured(_ layer: SkeletonLayer, forViewType type: SkeletonLayer.ViewType) + } + ``` + */ +class SkeletonsConfDelegate: SkeletonsConfigurationDelegate { + func layerDidConfigured(_ layer: SkeletonLayer, forViewType type: SkeletonLayer.ViewType) { + if case .imageView(_) = type { + layer.frame = .init(x: layer.frame.minX - 20, + y: layer.frame.minY - 20, + width: layer.frame.width, + height: layer.frame.height) + } + } +} + +let delegate = SkeletonsConfDelegate() +let confWithDelegate = SkeletonsConfiguration(configurationDelegate: delegate) + +/*: + ### Особенности + + Т.к. размеры view на основе которой строятся скелетоны не модифицируются, может возникнуть ситуация, когда одни скелетоны перекрывают другие. Например, когда размер view меньше ее скелетонов. В таких случаях как раз может помочь установка позиции или размеров в методе делегата `SkeletonsConfigurationDelegate` + + Также в качестве способа обойти такую ситуацию можно передавать во view моковые данные для увеличения ее размеров, чтобы размеры были хотя бы примерно похожи на размер скелетонов, как в примере: + */ +extension DefaultTitleSubtitleView: SkeletonsPresenter { + public var skeletonsConfiguration: SkeletonsConfiguration { + .init(labelConfiguration: .init(numberOfLines: 3)) + } +} + +let titleSubtitleView = DefaultTitleSubtitleView() +titleSubtitleView.configure(appearance: .make { + $0.titleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } + $0.subtitleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } +}) +titleSubtitleView.configure(with: .init(title: "very very long mock string to make multiple lines", + subtitle: "very very long mock string to make multiple lines")) + +titleSubtitleView.showSkeletons() + +DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { + titleSubtitleView.configure(with: .init(title: "normal data from a request", + subtitle: "normal data from a request")) + titleSubtitleView.hideSkeletons() +} + +//: ## Тестовый сконфигурированный контроллер +import PlaygroundSupport + +canShowAndHideController.view.frame = .init(origin: .zero, size: .init(width: 250, height: 100)) + +canShowAndHideController.hideSkeletons() +confWithLeftToRightAnim.labelConfiguration = .init(numberOfLines: 2) + +PlaygroundPage.current.liveView = canShowAndHideController + +canShowAndHideController.showSkeletons(viewsToSkeletons: nil, confWithLeftToRightAnim) diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground new file mode 100644 index 00000000..3debe4b3 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/contents.xcplayground @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.pbxproj b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.pbxproj new file mode 100644 index 00000000..52cfb0fd --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.pbxproj @@ -0,0 +1,395 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + B295B0E33533FFC3D833A6CB /* Pods_TIUIElements.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E0E28C6F64363C77CAE4662 /* Pods_TIUIElements.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2E10916FC4CAF67D840FC3D2 /* Pods-TIUIElements.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Target Support Files/Pods-TIUIElements/Pods-TIUIElements.release.xcconfig"; sourceTree = ""; }; + 8BACBE8322576CAD00266845 /* TIUIElements.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIUIElements.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8E0E28C6F64363C77CAE4662 /* Pods_TIUIElements.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIUIElements.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CB08C5B0C7051DCB015D3D9F /* Pods-TIUIElements.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIUIElements.debug.xcconfig"; path = "Target Support Files/Pods-TIUIElements/Pods-TIUIElements.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8BACBE8022576CAD00266845 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B295B0E33533FFC3D833A6CB /* Pods_TIUIElements.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6FA8D567F06C39C360B32325 /* Pods */ = { + isa = PBXGroup; + children = ( + 2E10916FC4CAF67D840FC3D2 /* Pods-TIUIElements.release.xcconfig */, + CB08C5B0C7051DCB015D3D9F /* Pods-TIUIElements.debug.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 7672C2F734E0BBEC76B58962 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8E0E28C6F64363C77CAE4662 /* Pods_TIUIElements.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8B39A26221D40F8700DE2643 = { + isa = PBXGroup; + children = ( + 8BACBE8422576CAD00266845 /* TIUIElements */, + 8B39A26C21D40F8700DE2643 /* Products */, + 6FA8D567F06C39C360B32325 /* Pods */, + 7672C2F734E0BBEC76B58962 /* Frameworks */, + ); + sourceTree = ""; + }; + 8B39A26C21D40F8700DE2643 /* Products */ = { + isa = PBXGroup; + children = ( + 8BACBE8322576CAD00266845 /* TIUIElements.framework */, + ); + name = Products; + sourceTree = ""; + }; + 8BACBE8422576CAD00266845 /* TIUIElements */ = { + isa = PBXGroup; + children = ( + 8BACBE8622576CAD00266845 /* Info.plist */, + ); + path = TIUIElements; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8BACBE7E22576CAD00266845 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8BACBE8222576CAD00266845 /* TIUIElements */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIUIElements" */; + buildPhases = ( + 9D2C80D787CBCD9B1EA728B0 /* [CP] Check Pods Manifest.lock */, + 8BACBE7E22576CAD00266845 /* Headers */, + 8BACBE7F22576CAD00266845 /* Sources */, + 8BACBE8022576CAD00266845 /* Frameworks */, + 8BACBE8122576CAD00266845 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TIUIElements; + productName = TIUIElements2; + productReference = 8BACBE8322576CAD00266845 /* TIUIElements.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 "TIUIElements" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B39A26221D40F8700DE2643; + productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8BACBE8222576CAD00266845 /* TIUIElements */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8BACBE8122576CAD00266845 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 9D2C80D787CBCD9B1EA728B0 /* [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-TIUIElements-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 = CB08C5B0C7051DCB015D3D9F /* Pods-TIUIElements.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIUIElements_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIUIElements/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.TIUIElements; + 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 = 2E10916FC4CAF67D840FC3D2 /* Pods-TIUIElements.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIUIElements_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIUIElements/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.TIUIElements; + 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 "TIUIElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B39A27721D40F8800DE2643 /* Debug */, + 8B39A27821D40F8800DE2643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIUIElements" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BACBE8822576CAD00266845 /* Debug */, + 8BACBE8922576CAD00266845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8B39A26321D40F8700DE2643 /* Project object */; +} diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..4fdc4a67 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/xcshareddata/xcschemes/TIUIElements.xcscheme b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/xcshareddata/xcschemes/TIUIElements.xcscheme new file mode 100644 index 00000000..2ac02e93 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcodeproj/xcshareddata/xcschemes/TIUIElements.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcworkspace/contents.xcworkspacedata b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..313f1fa5 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements/Info.plist b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements/Info.plist new file mode 100644 index 00000000..98d14f60 --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Copyright © 2019. The nef authors. + + diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/launcher b/TIUIElements/TIUIElements.app/Contents/MacOS/launcher new file mode 100755 index 00000000..dedc792f --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/launcher @@ -0,0 +1,6 @@ +#!/bin/bash + +workspace="TIUIElements.xcworkspace" +workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev) + +open "`pwd`/$workspacePath/$workspace" diff --git a/TIUIElements/TIUIElements.app/Contents/Resources/AppIcon.icns b/TIUIElements/TIUIElements.app/Contents/Resources/AppIcon.icns new file mode 100644 index 00000000..32814f1c Binary files /dev/null and b/TIUIElements/TIUIElements.app/Contents/Resources/AppIcon.icns differ diff --git a/TIUIElements/TIUIElements.app/Contents/Resources/Assets.car b/TIUIElements/TIUIElements.app/Contents/Resources/Assets.car new file mode 100644 index 00000000..79d9ea89 Binary files /dev/null and b/TIUIElements/TIUIElements.app/Contents/Resources/Assets.car differ diff --git a/TIUIElements/TIUIElements.playground b/TIUIElements/TIUIElements.playground new file mode 120000 index 00000000..254203c3 --- /dev/null +++ b/TIUIElements/TIUIElements.playground @@ -0,0 +1 @@ +TIUIElements.app/Contents/MacOS/TIUIElements.playground \ No newline at end of file diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 842543e0..929a4d54 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,11 +1,12 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Bunch of useful protocols and views.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru', + 'castlele' => 'nikita.semenov@touchin.ru' } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TIUIKitCore/Sources/Appearance/AppearanceConfigurable.swift b/TIUIKitCore/Sources/Appearance/AppearanceConfigurable.swift new file mode 100644 index 00000000..5c27df1b --- /dev/null +++ b/TIUIKitCore/Sources/Appearance/AppearanceConfigurable.swift @@ -0,0 +1,41 @@ +// +// 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 + +public protocol AppearanceConfigurable { + associatedtype Appearance: ViewAppearance + + func configure(appearance: Appearance) +} + +// MARK: - Creation methods + +extension AppearanceConfigurable { + @discardableResult + public func appearance(builder: ParameterClosure) -> Self { + let appearance = Appearance.defaultAppearance + builder(appearance) + configure(appearance: appearance) + return self + } +} diff --git a/TIUIKitCore/Sources/Appearance/UIViewBorder.swift b/TIUIKitCore/Sources/Appearance/UIViewBorder.swift new file mode 100644 index 00000000..00389343 --- /dev/null +++ b/TIUIKitCore/Sources/Appearance/UIViewBorder.swift @@ -0,0 +1,41 @@ +// +// 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 + +public struct UIViewBorder { + public var color: UIColor + public var width: CGFloat + public var cornerRadius: CGFloat + public var roundedCorners: CACornerMask + + public init(color: UIColor = .clear, + width: CGFloat = .zero, + cornerRadius: CGFloat = .zero, + roundedCorners: CACornerMask = []) { + + self.color = color + self.width = width + self.cornerRadius = cornerRadius + self.roundedCorners = roundedCorners + } +} diff --git a/TIUIKitCore/Sources/Appearance/UIViewShadow.swift b/TIUIKitCore/Sources/Appearance/UIViewShadow.swift new file mode 100644 index 00000000..3ea8addd --- /dev/null +++ b/TIUIKitCore/Sources/Appearance/UIViewShadow.swift @@ -0,0 +1,137 @@ +// +// 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 + +public struct UIViewShadow { + public var radius: CGFloat + public var offset: CGSize + public var color: UIColor + public var opacity: Float + + public init(radius: CGFloat = .zero, + offset: CGSize = .zero, + color: UIColor = .clear, + opacity: Float = .zero) { + + self.radius = radius + self.offset = offset + self.color = color + self.opacity = opacity + } + + public init(@UIViewShadowBuilder builder: () -> [ShadowComponent]) { + let components: [ShadowComponent] = builder() + + var radius: CGFloat = .zero + var offset: CGSize = .zero + var color: UIColor = .clear + var opacity: Float = .zero + + for component in components { + switch component { + case let component as Radius: + radius = component.radius + + case let component as Offset: + offset = component.offset + + case let component as RGBA: + color = component.uiColor + + case let component as Color: + color = component.color + + case let component as Opacity: + opacity = component.opacity + + default: + continue + } + } + + if opacity == .zero, color != .clear { + opacity = Float(color.cgColor.alpha) + } + + self.init(radius: radius, offset: offset, color: color, opacity: opacity) + } +} + +// MARK: - UIViewShadowBuilder + +@resultBuilder +public struct UIViewShadowBuilder { + public static func buildBlock(_ components: ShadowComponent...) -> [ShadowComponent] { + components + } +} + +// MARK: - ShadowComponents + +public protocol ShadowComponent { + +} + +public struct Radius: ShadowComponent { + public let radius: CGFloat + + public init(_ radius: CGFloat) { + self.radius = radius + } +} + +public struct Offset: ShadowComponent { + public let offset: CGSize + + public init(_ offset: CGSize) { + self.offset = offset + } + + public init(_ x: CGFloat, _ y: CGFloat) { + self.offset = .init(width: x, height: y) + } +} + +public struct Color: ShadowComponent { + public let color: UIColor + + public init(_ color: UIColor) { + self.color = color + } +} + +public struct RGBA: ShadowComponent { + public let uiColor: UIColor + + public init(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat, _ a: CGFloat) { + self.uiColor = .init(red: r, green: g, blue: b, alpha: a) + } +} + +public struct Opacity: ShadowComponent { + public let opacity: Float + + public init(_ opacity: Float) { + self.opacity = opacity + } +} diff --git a/TIUIKitCore/Sources/Appearance/ViewAppearance.swift b/TIUIKitCore/Sources/Appearance/ViewAppearance.swift new file mode 100644 index 00000000..322d3641 --- /dev/null +++ b/TIUIKitCore/Sources/Appearance/ViewAppearance.swift @@ -0,0 +1,72 @@ +// +// 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 + +public protocol ViewAppearance { + associatedtype Layout: ViewLayout + + static var defaultAppearance: Self { get } + + var layout: Layout { get } + var backgroundColor: UIColor { get } + var border: UIViewBorder { get } + var shadow: UIViewShadow? { get } +} + +// MARK: - ViewAppearance Variations + +public protocol WrappedViewAppearance: ViewAppearance where Layout: WrappedViewLayout {} + +public protocol WrappedViewHolderAppearance: ViewAppearance { + associatedtype SubviewAppearance: WrappedViewAppearance + + var subviewAppearance: SubviewAppearance { get } +} + +// MARK: - Creation methods + +extension ViewAppearance { + public static func make(builder: (Self) -> Void) -> Self { + let appearance = Self.defaultAppearance + builder(appearance) + return appearance + } + + public static func callAsFunction(builder: (Self) -> Void) -> Self { + let appearance = Self.defaultAppearance + builder(appearance) + return appearance + } + + @discardableResult + public func update(builder: (Self) -> Void) -> Self { + builder(self) + return self + } + + @discardableResult + public func callAsFunction(builder: (Self) -> Void) -> Self { + builder(self) + return self + } +} diff --git a/TIUIKitCore/Sources/Appearance/ViewLayout.swift b/TIUIKitCore/Sources/Appearance/ViewLayout.swift new file mode 100644 index 00000000..24c51307 --- /dev/null +++ b/TIUIKitCore/Sources/Appearance/ViewLayout.swift @@ -0,0 +1,59 @@ +// +// 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 UIKit + +public protocol ViewLayout { + static var defaultLayout: Self { get } +} + +// MARK: - ViewLayout Variations + +public protocol SizeViewLayout: ViewLayout { + var size: CGSize { get } +} + +public protocol WrappedViewLayout: SizeViewLayout { + var insets: UIEdgeInsets { get } + var centerOffset: UIOffset { get } +} + +public protocol SpacedWrappedViewLayout: WrappedViewLayout { + var spacing: CGFloat { get } +} + +// MARK: - Creation methods + +extension ViewLayout { + @discardableResult + public func update(builder: ParameterClosure) -> Self { + builder(self) + return self + } + + @discardableResult + public func callAsFunction(builder: ParameterClosure) -> Self { + builder(self) + return self + } +} diff --git a/TIUIKitCore/Sources/Extensions/CoreGraphics/CGSize+Extensions.swift b/TIUIKitCore/Sources/Extensions/CoreGraphics/CGSize+Extensions.swift new file mode 100644 index 00000000..7c05b946 --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/CoreGraphics/CGSize+Extensions.swift @@ -0,0 +1,38 @@ +// +// 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 CoreGraphics.CGBase + +public extension CGSize { + static var infinity: Self { + Self(width: CGFloat.infinity, height: CGFloat.infinity) + } + + static func fixedWidth(_ width: CGFloat) -> Self { + Self(width: width, height: CGFloat.infinity) + } + + static func fixedHeight(_ height: CGFloat) -> Self { + Self(width: CGFloat.infinity, height: height) + } +} + diff --git a/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift b/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift new file mode 100644 index 00000000..8eb4257c --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift @@ -0,0 +1,66 @@ +// +// 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 + +public extension UIEdgeInsets { + + // MARK: - Factory methods + + static func edges(_ insets: CGFloat) -> UIEdgeInsets { + .init(top: insets, left: insets, bottom: insets, right: insets) + } + + static func horizontal(_ insets: CGFloat) -> UIEdgeInsets { + .init(top: .zero, left: insets, bottom: .zero, right: insets) + } + + static func vertical(_ insets: CGFloat) -> UIEdgeInsets { + .init(top: insets, left: .zero, bottom: insets, right: .zero) + } + + static func horizontal(left: CGFloat = .zero, right: CGFloat = .zero) -> UIEdgeInsets { + .init(top: .zero, left: left, bottom: .zero, right: right) + } + + static func vertical(top: CGFloat = .zero, bottom: CGFloat = .zero) -> UIEdgeInsets { + .init(top: top, left: .zero, bottom: bottom, right: .zero) + } + + // MARK: - Instance methods + + func horizontal(_ insets: CGFloat) -> UIEdgeInsets { + .init(top: top, left: insets, bottom: bottom, right: insets) + } + + func vertical(_ insets: CGFloat) -> UIEdgeInsets { + .init(top: insets, left: left, bottom: insets, right: right) + } + + func horizontal(left: CGFloat = .zero, right: CGFloat = .zero) -> UIEdgeInsets { + .init(top: top, left: left, bottom: bottom, right: right) + } + + func vertical(top: CGFloat = .zero, bottom: CGFloat = .zero) -> UIEdgeInsets { + .init(top: top, left: left, bottom: bottom, right: right) + } +} diff --git a/TIUIKitCore/Sources/Extensions/UIKit/UIOffset+Extensions.swift b/TIUIKitCore/Sources/Extensions/UIKit/UIOffset+Extensions.swift new file mode 100644 index 00000000..d1f6e938 --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/UIKit/UIOffset+Extensions.swift @@ -0,0 +1,37 @@ +// +// 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 + +public extension UIOffset { + static var nan: Self { + Self(horizontal: .nan, vertical: .nan) + } + + static func centerVertical(_ verticalOffset: CGFloat = .zero) -> Self { + Self(horizontal: .nan, vertical: verticalOffset) + } + + static func centerHorizontal(_ horizontalOffset: CGFloat = .zero) -> Self { + Self(horizontal: horizontalOffset, vertical: .nan) + } +} diff --git a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift index d1b7bbe8..719578fb 100644 --- a/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift +++ b/TIUIKitCore/Sources/TextAttributes/BaseTextAttributes/BaseTextAttributes.swift @@ -152,8 +152,34 @@ open class BaseTextAttributes { attributedTextConfiguration: { textView.attributedText = $0 }) } + open func configure(button: UIButton, with string: String? = nil) { + if #available(iOS 15, *) { + var configuration = button.configuration ?? UIButton.Configuration.plain() + + if let title = string { + button.setTitle(nil, for: .normal) + button.setAttributedTitle(nil, for: .normal) + + configuration.attributedTitle = attributedString(for: title) + button.configuration = configuration + } + } else { + button.setTitle(string, for: .normal) + button.setTitleColor(color, for: .normal) + + if let label = button.titleLabel { + configure(label: label) + } + } + } + // MARK: - Attributed string manipulation + @available(iOS 15, *) + open func attributedString(for string: String) -> AttributedString { + AttributedString(attributedString(for: string)) + } + open func attributedString(for string: String) -> NSAttributedString { NSAttributedString(string: string, attributes: attributedStringAttributes) } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 978a6563..b62c7eb6 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,11 +1,12 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Core UI elements: protocols, views and helpers.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru', + 'castlele' => 'nikita.semenov@touchin.ru' } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.swift_versions = ['5.3'] diff --git a/TILogging/Sources/Logger/LoggerRepresentable.swift b/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift similarity index 88% rename from TILogging/Sources/Logger/LoggerRepresentable.swift rename to TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift index 5337fcfe..cbe68ac9 100644 --- a/TILogging/Sources/Logger/LoggerRepresentable.swift +++ b/TIWebView/Sources/ErrorHandler/BaseWebViewErrorHandler.swift @@ -20,8 +20,9 @@ // THE SOFTWARE. // -import os +open class BaseWebViewErrorHandler: WebViewErrorHandler { -public protocol LoggerRepresentable { - func log(_ message: StaticString, log: OSLog?, type: OSLogType, _ arguments: CVarArg...) + public init() { + + } } diff --git a/TIWebView/Sources/ErrorHandler/WebViewError/WebViewError.swift b/TIWebView/Sources/ErrorHandler/WebViewError/WebViewError.swift new file mode 100644 index 00000000..451d2cdb --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewError/WebViewError.swift @@ -0,0 +1,27 @@ +// +// 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 WebViewError: Error { + var sourceURL: URL? { get } +} diff --git a/TIWebView/Sources/ErrorHandler/WebViewError/WebViewJSError.swift b/TIWebView/Sources/ErrorHandler/WebViewError/WebViewJSError.swift new file mode 100644 index 00000000..807c06ff --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewError/WebViewJSError.swift @@ -0,0 +1,71 @@ +// +// 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 WebViewJSError: WebViewError, Codable { + + public enum CodingKeys: String, CodingKey { + case sourceURL + case name + case message + case lineNumber = "line" + case columnNumber = "column" + case stackTrace = "stack" + } + + public let sourceURL: URL? + public let name: String? + public let message: String? + public let lineNumber: Int? + public let columnNumber: Int? + public let stackTrace: String? + + public init(sourceURL: String?, + name: String?, + message: String?, + lineNumber: Int?, + columnNumber: Int?, + stackTrace: String?) { + + if let sourceURL = sourceURL { + self.sourceURL = URL(string: sourceURL) + } else { + self.sourceURL = nil + } + + self.name = name + self.message = message + self.lineNumber = lineNumber + self.columnNumber = columnNumber + self.stackTrace = stackTrace + } + + public init?(from json: [String: Any], jsonDecoder: JSONDecoder = .init()) { + guard let data = try? JSONSerialization.data(withJSONObject: json), + let error = try? jsonDecoder.decode(WebViewJSError.self, from: data) else { + return nil + } + + self = error + } +} diff --git a/TIWebView/Sources/ErrorHandler/WebViewError/WebViewLoadingError.swift b/TIWebView/Sources/ErrorHandler/WebViewError/WebViewLoadingError.swift new file mode 100644 index 00000000..afbd5fb8 --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewError/WebViewLoadingError.swift @@ -0,0 +1,33 @@ +// +// 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 WebViewLoadingError: WebViewError { + public let sourceURL: URL? + public let innerError: Error + + public init(sourceURL: URL?, innerError: Error) { + self.sourceURL = sourceURL + self.innerError = innerError + } +} diff --git a/TIWebView/Sources/ErrorHandler/WebViewErrorConstants.swift b/TIWebView/Sources/ErrorHandler/WebViewErrorConstants.swift new file mode 100644 index 00000000..b4ed3e94 --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewErrorConstants.swift @@ -0,0 +1,27 @@ +// +// 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 WebViewErrorConstants { + static var errorMessageName: String { + "error" + } +} diff --git a/TIWebView/Sources/ErrorHandler/WebViewErrorHandler.swift b/TIWebView/Sources/ErrorHandler/WebViewErrorHandler.swift new file mode 100644 index 00000000..7a58b748 --- /dev/null +++ b/TIWebView/Sources/ErrorHandler/WebViewErrorHandler.swift @@ -0,0 +1,31 @@ +// +// 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 WebViewErrorHandler { + func didReceiveError(_ error: WebViewError) +} + +public extension WebViewErrorHandler { + func didReceiveError(_ error: WebViewError) { + // override in subclasses + } +} diff --git a/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift b/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift new file mode 100644 index 00000000..b7eb828a --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/BaseWebViewNavigator.swift @@ -0,0 +1,34 @@ +// +// 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 BaseWebViewNavigator: WebViewNavigator { + + public var navigationPolicyMap: [NavigationPolicy] + + public init(navigationPolicyMap: [NavigationPolicy]) { + self.navigationPolicyMap = navigationPolicyMap + } + + public convenience init() { + self.init(navigationPolicyMap: []) + } +} diff --git a/TIWebView/Sources/NavigationHandler/Helpers/URL+Validation.swift b/TIWebView/Sources/NavigationHandler/Helpers/URL+Validation.swift new file mode 100644 index 00000000..bcbe76ca --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/Helpers/URL+Validation.swift @@ -0,0 +1,46 @@ +// +// 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 + +extension URL { + func matches(_ regex: NSRegularExpression) -> Bool { + let range = NSRange(location: 0, length: absoluteString.utf16.count) + return regex.firstMatch(in: absoluteString, range: range) != nil + } + + func validate(by component: URLComponent) -> Bool { + switch component { + case let .host(host): + return self.host == host + + case let .absolutePath(path): + return absoluteString == path + + case let .query(query): + if let urlQuery = self.query { + return urlQuery.contains(query) + } + return false + } + } +} diff --git a/TIWebView/Sources/NavigationHandler/Helpers/URLComponent.swift b/TIWebView/Sources/NavigationHandler/Helpers/URLComponent.swift new file mode 100644 index 00000000..df504403 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/Helpers/URLComponent.swift @@ -0,0 +1,27 @@ +// +// 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 URLComponent { + case host(String) + case absolutePath(String) + case query(String) +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationPolicy/AlwaysAllowNavigationPolicy.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/AlwaysAllowNavigationPolicy.swift new file mode 100644 index 00000000..4916580b --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/AlwaysAllowNavigationPolicy.swift @@ -0,0 +1,35 @@ +// +// 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 enum WebKit.WKNavigationActionPolicy + +open class AlwaysAllowNavigationPolicy: NavigationPolicy { + + public init() { + + } + + open func policy(for url: URL) -> WKNavigationActionPolicy { + .allow + } +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationPolicy/NavigationPolicy.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/NavigationPolicy.swift new file mode 100644 index 00000000..57b013d4 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/NavigationPolicy.swift @@ -0,0 +1,28 @@ +// +// 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 enum WebKit.WKNavigationActionPolicy + +public protocol NavigationPolicy { + func policy(for url: URL) -> WKNavigationActionPolicy +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationPolicy/RegexNavigationPolicy.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/RegexNavigationPolicy.swift new file mode 100644 index 00000000..40d9a8c7 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/RegexNavigationPolicy.swift @@ -0,0 +1,51 @@ +// +// 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 enum WebKit.WKNavigationActionPolicy + +open class RegexNavigationPolicy: AlwaysAllowNavigationPolicy { + + public var regex: NSRegularExpression + + // MARK: - Init + + public init(regex: NSRegularExpression) { + self.regex = regex + + super.init() + } + + public convenience init?(stringRegex: String) { + guard let regex = try? NSRegularExpression(pattern: stringRegex) else { + return nil + } + + self.init(regex: regex) + } + + // MARK: - NavigationPolicy + + open override func policy(for url: URL) -> WKNavigationActionPolicy { + url.matches(regex) ? .allow : .cancel + } +} diff --git a/TIWebView/Sources/NavigationHandler/NavigationPolicy/URLComponentsNavigationPolicy.swift b/TIWebView/Sources/NavigationHandler/NavigationPolicy/URLComponentsNavigationPolicy.swift new file mode 100644 index 00000000..568ddb11 --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/NavigationPolicy/URLComponentsNavigationPolicy.swift @@ -0,0 +1,44 @@ +// +// 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 enum WebKit.WKNavigationActionPolicy + +/// Compares URL with combination of URL components. +open class URLComponentsNavigationPolicy: AlwaysAllowNavigationPolicy { + + public var components: [URLComponent] + + // MARK: - Init + + public init(components: [URLComponent]) { + self.components = components + + super.init() + } + + // MARK: - NavigationPolicy + + open override func policy(for url: URL) -> WKNavigationActionPolicy { + components.allSatisfy { url.validate(by: $0) } ? .allow : .cancel + } +} diff --git a/TIWebView/Sources/NavigationHandler/WebViewNavigator.swift b/TIWebView/Sources/NavigationHandler/WebViewNavigator.swift new file mode 100644 index 00000000..8fd653ce --- /dev/null +++ b/TIWebView/Sources/NavigationHandler/WebViewNavigator.swift @@ -0,0 +1,41 @@ +// +// 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 enum WebKit.WKNavigationActionPolicy + +public protocol WebViewNavigator { + var navigationPolicyMap: [NavigationPolicy] { get set } + + func shouldNavigate(to url: URL) -> WKNavigationActionPolicy +} + +public extension WebViewNavigator { + func shouldNavigate(to url: URL) -> WKNavigationActionPolicy { + guard !navigationPolicyMap.isEmpty else { + return .cancel + } + + let allowPolicy = navigationPolicyMap.filter { $0.policy(for: url) == .allow } + return allowPolicy.isEmpty ? .cancel : .allow + } +} diff --git a/TIWebView/Sources/StateDelegate/BaseWebViewStateHandler.swift b/TIWebView/Sources/StateDelegate/BaseWebViewStateHandler.swift new file mode 100644 index 00000000..6cd66fae --- /dev/null +++ b/TIWebView/Sources/StateDelegate/BaseWebViewStateHandler.swift @@ -0,0 +1,64 @@ +// +// 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 WebKit + +open class BaseWebViewStateHandler: NSObject, WebViewStateHandler { + + public weak var viewModel: WebViewModel? + + // MARK: - WebViewStateHandler + + open func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + guard let url = navigationAction.request.url else { + return decisionHandler(.cancel) + } + + let decision = viewModel?.shouldNavigate(to: url) ?? .cancel + decisionHandler(decision) + } + + open func webView(_ webView: WKWebView, didCommit navigation: WKNavigation?) { + // override in subclasses + } + + open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { + viewModel?.makeUrlInjection(into: webView) + } + + open func webView(_ webView: WKWebView, + didFailProvisionalNavigation navigation: WKNavigation?, + withError error: Error) { + + viewModel?.handleError(error, url: webView.url) + } + + open func webView(_ webView: WKWebView, + didFail navigation: WKNavigation?, + withError error: Error) { + + viewModel?.handleError(error, url: webView.url) + } +} diff --git a/TIWebView/Sources/StateDelegate/WebViewStateHandler.swift b/TIWebView/Sources/StateDelegate/WebViewStateHandler.swift new file mode 100644 index 00000000..6d084f15 --- /dev/null +++ b/TIWebView/Sources/StateDelegate/WebViewStateHandler.swift @@ -0,0 +1,27 @@ +// +// 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 protocol WebKit.WKNavigationDelegate + +public protocol WebViewStateHandler: WKNavigationDelegate { + var viewModel: WebViewModel? { get set } +} diff --git a/TIWebView/Sources/URLInjector/BaseWebViewUrlInjector.swift b/TIWebView/Sources/URLInjector/BaseWebViewUrlInjector.swift new file mode 100644 index 00000000..2429a2d7 --- /dev/null +++ b/TIWebView/Sources/URLInjector/BaseWebViewUrlInjector.swift @@ -0,0 +1,37 @@ +// +// 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 class WebKit.WKWebView + +open class BaseWebViewUrlInjector: WebViewUrlInjector { + + public var injection: URLInjection + + public init(injection: URLInjection) { + self.injection = injection + } + + public convenience init() { + self.init(injection: [:]) + } +} diff --git a/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift b/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift new file mode 100644 index 00000000..6c32c718 --- /dev/null +++ b/TIWebView/Sources/URLInjector/Helpers/URL+Comparator.swift @@ -0,0 +1,44 @@ +// +// 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 URL { + func compare(by comparator: WebViewUrlComparator) -> Bool { + switch comparator { + case .any: + return true + + case let .absolutePath(path): + return compare(by: .absolutePath(path)) + + case let .host(host): + return compare(by: .host(host)) + + case let .query(query): + return compare(by: .query(query)) + + case let .regex(nsRegex): + return matches(nsRegex) + } + } +} diff --git a/TIWebView/Sources/URLInjector/WebViewUrlComparator.swift b/TIWebView/Sources/URLInjector/WebViewUrlComparator.swift new file mode 100644 index 00000000..39545913 --- /dev/null +++ b/TIWebView/Sources/URLInjector/WebViewUrlComparator.swift @@ -0,0 +1,31 @@ +// +// 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 WebViewUrlComparator: Hashable { + case any + case absolutePath(String) + case host(String) + case query(String) + case regex(NSRegularExpression) +} diff --git a/TIWebView/Sources/URLInjector/WebViewUrlInjection.swift b/TIWebView/Sources/URLInjector/WebViewUrlInjection.swift new file mode 100644 index 00000000..d2c7f228 --- /dev/null +++ b/TIWebView/Sources/URLInjector/WebViewUrlInjection.swift @@ -0,0 +1,29 @@ +// +// 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 WebViewUrlInjection { + case css(String) + case cssForFile(URL) + case javaScript(String) +} diff --git a/TIWebView/Sources/URLInjector/WebViewUrlInjector.swift b/TIWebView/Sources/URLInjector/WebViewUrlInjector.swift new file mode 100644 index 00000000..aa63fc6c --- /dev/null +++ b/TIWebView/Sources/URLInjector/WebViewUrlInjector.swift @@ -0,0 +1,88 @@ +// +// 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 class WebKit.WKWebView + +public typealias URLInjection = [WebViewUrlComparator: [WebViewUrlInjection]] + +public protocol WebViewUrlInjector { + var injection: URLInjection { get set } + + func inject(into webView: WKWebView) +} + +public extension WebViewUrlInjector { + + func inject(into webView: WKWebView) { + guard !injection.isEmpty, let url = webView.url else { + return + } + + injection.forEach { (comparator, injections) in + guard url.compare(by: comparator) else { + return + } + + injections.forEach { evaluteInjection(onWebView: webView, injection: $0) } + } + } + + // MARK: - Helper methods + + private func evaluteInjection(onWebView webView: WKWebView, injection: WebViewUrlInjection) { + guard let jsScript = makeJsScript(fromInjection: injection) else { + return + } + + webView.evaluateJavaScript(jsScript, completionHandler: nil) + } + + private func makeJsScript(fromInjection injection: WebViewUrlInjection) -> String? { + switch injection { + case let .css(css): + return cssJsScript(css: css) + + case let .cssForFile(url): + let css = try? String(contentsOf: url) + .components(separatedBy: .newlines) + .joined() + + if let css = css, !css.isEmpty { + return cssJsScript(css: css) + } + + return nil + + case let .javaScript(script): + return script + } + } + + private func cssJsScript(css: String) -> String { + """ + var style = document.createElement('style'); + style.innerHTML = '\(css)'; + document.head.appendChild(style); + """ + } +} diff --git a/TIWebView/Sources/Views/BaseInitializableWebView.swift b/TIWebView/Sources/Views/BaseInitializableWebView.swift new file mode 100644 index 00000000..4782cd1b --- /dev/null +++ b/TIWebView/Sources/Views/BaseInitializableWebView.swift @@ -0,0 +1,92 @@ +// +// 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 TISwiftUtils +import TIUIKitCore +import WebKit + +open class BaseInitializableWebView: WKWebView, + InitializableViewProtocol, + ConfigurableView { + + public var stateHandler: WebViewStateHandler + public var viewModel: WebViewModel? { + didSet { + stateHandler.viewModel = viewModel + } + } + + // MARK: - Init + + public init(stateHandler: WebViewStateHandler = BaseWebViewStateHandler()) { + self.stateHandler = stateHandler + + super.init(frame: .zero, configuration: .init()) + + initializeView() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - InitializableViewProtocol + + public func addViews() { + // override in subviews + } + + public func configureLayout() { + // override in subviews + } + + public func bindViews() { + navigationDelegate = stateHandler + } + + public func configureAppearance() { + // override in subviews + } + + public func localize() { + // override in subviews + } + + // MARK: - ConfigurableView + + open func configure(with viewModel: WebViewModel) { + self.viewModel = viewModel + + configuration.userContentController.add(viewModel, name: WebViewErrorConstants.errorMessageName) + } + + // MARK: - Public methods + + public func subscribe(onProgress: ParameterClosure?) -> NSKeyValueObservation { + observe(\.estimatedProgress, options: [.new]) { webView, change in + if webView.isLoading, let newValue = change.newValue { + onProgress?(newValue) + } + } + } +} diff --git a/TIWebView/Sources/Views/ViewModels/DefaultWebViewModel.swift b/TIWebView/Sources/Views/ViewModels/DefaultWebViewModel.swift new file mode 100644 index 00000000..6caaf1de --- /dev/null +++ b/TIWebView/Sources/Views/ViewModels/DefaultWebViewModel.swift @@ -0,0 +1,63 @@ +// +// 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 WebKit + +open class DefaultWebViewModel: NSObject, WebViewModel { + + public var injector: WebViewUrlInjector + public var navigator: WebViewNavigator + public var errorHandler: WebViewErrorHandler + + // MARK: - Init + + public init(injector: WebViewUrlInjector = BaseWebViewUrlInjector(), + navigator: WebViewNavigator = BaseWebViewNavigator(), + errorHandler: WebViewErrorHandler = BaseWebViewErrorHandler()) { + + self.injector = injector + self.navigator = navigator + self.errorHandler = errorHandler + + super.init() + } + + // MARK: - WKScriptMessageHandler + + open func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { + + if message.name == WebViewErrorConstants.errorMessageName, + let error = parseError(message) { + errorHandler.didReceiveError(error) + } + } + + // MARK: - Private methods + + private func parseError(_ message: WKScriptMessage) -> WebViewError? { + guard let body = message.body as? [String: Any] else { + return nil + } + return WebViewJSError(from: body) + } +} diff --git a/TIWebView/Sources/Views/ViewModels/WebViewModel.swift b/TIWebView/Sources/Views/ViewModels/WebViewModel.swift new file mode 100644 index 00000000..409d21b4 --- /dev/null +++ b/TIWebView/Sources/Views/ViewModels/WebViewModel.swift @@ -0,0 +1,49 @@ +// +// 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 WebKit + +public protocol WebViewModel: WKScriptMessageHandler { + var injector: WebViewUrlInjector { get } + var navigator: WebViewNavigator { get } + var errorHandler: WebViewErrorHandler { get } + + func makeUrlInjection(into webView: WKWebView) + func shouldNavigate(to url: URL) -> WKNavigationActionPolicy + func handleError(_ error: Error, url: URL?) +} + +public extension WebViewModel { + + func makeUrlInjection(into webView: WKWebView) { + injector.inject(into: webView) + } + + func shouldNavigate(to url: URL) -> WKNavigationActionPolicy { + navigator.shouldNavigate(to: url) + } + + func handleError(_ error: Error, url: URL?) { + let errorModel = WebViewLoadingError(sourceURL: url, innerError: error) + errorHandler.didReceiveError(errorModel) + } +} diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec new file mode 100644 index 00000000..9e3654b8 --- /dev/null +++ b/TIWebView/TIWebView.podspec @@ -0,0 +1,19 @@ +Pod::Spec.new do |s| + s.name = 'TIWebView' + s.version = '1.39.0' + s.summary = 'Universal web view API' + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru', + 'castlele' => 'nikita.semenov@touchin.ru' } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '11.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + + s.dependency 'TIUIKitCore', s.version.to_s + s.dependency 'TISwiftUtils', s.version.to_s + +end diff --git a/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift b/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift index f5b42af4..87a2bf73 100644 --- a/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift +++ b/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift @@ -25,7 +25,11 @@ import YandexMapsMobile import UIKit import CoreLocation -open class YandexClusterPlacemarkManager: BasePlacemarkManager], [YMKPoint]>, YMKClusterListener, YMKClusterTapListener { +open class YandexClusterPlacemarkManager: BaseClusterPlacemarkManager, + [YMKPoint]>, + YMKClusterListener, + YMKClusterTapListener { private var placemarkCollection: YMKClusterizedPlacemarkCollection? @@ -33,7 +37,8 @@ open class YandexClusterPlacemarkManager: BasePlacemarkManager: BasePlacemarkManager: BasePlacemarkManager [YandexPlacemarkManager] { @@ -92,7 +97,7 @@ open class YandexClusterPlacemarkManager: BasePlacemarkManager: BasePlacemarkManager, YMKMapObjectTapListener { - public let position: YMKPoint +open class YandexPlacemarkManager: BaseItemPlacemarkManager, + YMKMapObjectTapListener { + + /// The current state of a manager's placemark + override public var state: MarkerState { + didSet { + guard let placemark = placemark else { + return + } + + if let customIcon = iconFactory?.markerIcon(for: dataModel, state: state) { + placemark.setIconWith(customIcon) + } + } + } public init(dataModel: Model, position: YMKPoint, iconFactory: AnyMarkerIconFactory, tapHandler: TapHandlerClosure?) { - self.position = position - - super.init(dataModel: dataModel, + super.init(placemarkPosition: position, + dataModel: dataModel, iconFactory: iconFactory, tapHandler: tapHandler) } @@ -41,16 +53,27 @@ open class YandexPlacemarkManager: BasePlacemarkManager Bool { - tapHandler?(dataModel, point) ?? false + // Tap handling and receiving a result flag + let isTapHandled = tapHandler?(dataModel, point) ?? false + + + // If a tap was handled successfully then additionally update pin state for appearance configuration + if isTapHandled { + state = .selected + } + + return isTapHandled } // MARK: - PlacemarkManager override open func configure(placemark: YMKPlacemarkMapObject) { - placemark.addTapListener(with: self) + super.configure(placemark: placemark) - if let customIcon = iconFactory?.markerIcon(for: dataModel) { - placemark.setIconWith(customIcon) - } + // Setting initial value of the current placemark state + self.state = .default + + // Setting tap listener for a pin tap handling + placemark.addTapListener(with: self) } } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 755a8280..d832db7e 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,13 +1,13 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.33.0' + s.version = '1.39.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' - s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.homepage = 'https://gitlab.ti/touchinstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } - s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + s.source = { :git => 'https://gitlab.ti/touchinstinct/LeadKit.git', :tag => s.version.to_s } - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '12.0' s.swift_versions = ['5.3'] s.source_files = s.name + '/Sources/**/*' @@ -17,5 +17,5 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } s.dependency 'TIMapUtils', s.version.to_s - s.dependency 'YandexMapsMobile', '4.0.0-lite' + s.dependency 'YandexMapsMobile', '4.3.1-lite' end diff --git a/docs/tifoundationutils/asyncoperation.md b/docs/tifoundationutils/asyncoperation.md new file mode 100644 index 00000000..5a2175e1 --- /dev/null +++ b/docs/tifoundationutils/asyncoperation.md @@ -0,0 +1,74 @@ + +# `AsyncOperation` - generic сабкласс Operation + +Позволяет запускать: + +- асинхронный код внутри операции +- собирать цепочки из операций +- подписываться на результат выполнения + +## Базовые операции + + "Из коробки", на данный момент, доступен всего один сабкласс асинхронной операции, потому что больше обычно и не нужно. + Но можно наследоваться и создавать собственные сабклассы при необходимости. + +### `ClosureAsyncOperation` + + Операция принимающая некий closure, который по окончании своей работы вызовет completion, переданный ему параметром + +```swift +import Foundation +import TIFoundationUtils + +let intResultOperation = ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(3)) { + completion(.success(1)) + } + return Cancellables.nonCancellable() +} +``` + +## Базовые операторы + + На данный момент реализовано всего два оператора: + + - `map(mapOutput:mapFailure:)` - конвертирует ResultType в новый NewResultType и ErrorType в новый NewErrorType + - `observe(onSuccess:onFailure)` - просто вызывает переданные callback'и при получении результата или ошибки + + +### Пример запуска асинхронных операци с применением операторов в последовательной очереди и вывод результатов + +```swift +let operationQueue = OperationQueue() +operationQueue.maxConcurrentOperationCount = 1 + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(3)) { + completion(.success(1)) + } + return Cancellables.nonCancellable() +} +.map { $0 * 2 } +.observe(onSuccess: { result in + debugPrint("Async operation one has finished with \(result)") +}) +.add(to: operationQueue) + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + completion(.success("Success")) + } + return Cancellables.nonCancellable() +} +.observe(onSuccess: { result in + debugPrint("Async operation two has finished with \(result)") +}) +.add(to: operationQueue) +``` + + В консоли будет выведено: + + ``` + "Async operation one has finished with 2" + "Async operation two has finished with Success" + ``` diff --git a/docs/tiuielements/skeletons.md b/docs/tiuielements/skeletons.md new file mode 100644 index 00000000..945c849a --- /dev/null +++ b/docs/tiuielements/skeletons.md @@ -0,0 +1,340 @@ + +# Skeletons API + + При импорте _TIUIElements_ вы можете использовать API для показа скелетонов. + +## Принцип работы + + При использовании методов показа скелетонов: + 1. происходит скрытие всех subview в иерархии той view, на которой был вызван метод + 2. далее происходит проход по view, которые можно сконвертировать в скелетоны (список либо определяется пользователем, либо конвертация будет происходить автоматически), создается `CALayer` типа `SkeletonLayer`, представляющий конвертируемую view + 3. поверх view с которой начался показ, добавляются все созданные `SkeletonLayer` + + > Таким образом скелетоны не модифицируют размеры view и не изменяют ее положение + +## Как начать пользоваться + + Базовая настройка для показа скелетонов не требуется. `UIView` и `UIViewController` уже имеют все необходимые методы для работы: + - `showSkeletons(viewsToSkeletons:_:)` : используется для показа скелетонов, если была передана конфигурация анимации, то она запустится автоматически. `viewsToSkeletons` - опциональный массив `UIView`, определяющий, какие вью будут конвертироваться в скелетоны. Если nil, то проход будет осуществляться по списку subview + - `hideSkeletons()` : используется для скрытия скелетонов + - `startAnimation()` : используется для старта анимации на скелетонах (если анимания не была сконфигурирована в методе `showSkeletons(viewsToSkeletons:_:)` то ничего не произойдет) + - `stopAnimation()` : используется для остановки анимации на скелетонах + +```swift +import TIUIKitCore +import TIUIElements +import UIKit + +class CanShowAndHideSkeletons: BaseInitializableViewController { + + private let imageView = UIImageView(image: UIImage(systemName: "apple.logo")) + private let label = UILabel() + private let button = UIButton(type: .custom) + + override func addViews() { + super.addViews() + + view.addSubviews(imageView, label, button) + } + + override func configureLayout() { + super.configureLayout() + + [imageView, label, button] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + imageView.heightAnchor.constraint(equalToConstant: 40), + imageView.widthAnchor.constraint(equalToConstant: 40), + imageView.centerYAnchor.constraint(equalTo: label.centerYAnchor), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + + label.topAnchor.constraint(equalTo: view.topAnchor), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor), + label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), + label.heightAnchor.constraint(equalToConstant: 60), + + button.leadingAnchor.constraint(equalTo: view.leadingAnchor), + button.topAnchor.constraint(equalTo: label.bottomAnchor), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + override func bindViews() { + super.bindViews() + + button.addTarget(self, action: #selector(toggleSkeletons), for: .touchUpInside) + } + + override func configureAppearance() { + super.configureAppearance() + + label.text = "Hello from SkeletonableViewController" + button.setTitle("show skeletons", for: .normal) + + let textAttributes = BaseTextAttributes(font: .systemFont(ofSize: 25), color: .black, alignment: .natural, isMultiline: false) + + view.configureUIView(appearance: UIView.DefaultAppearance(backgroundColor: .white)) + + label.configureUILabel(appearance: UILabel.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + + button.configureUIButton(appearance: UIButton.DefaultAppearance.make { + $0.textAttributes = textAttributes + }) + } + + @objc private func toggleSkeletons() { + // Т.к. передается nil, скелетониться будут все subview (в данном случае view.subview == [button, label, imageView]) + showSkeletons(viewsToSkeletons: nil, .init()) + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in + self?.hideSkeletons() + } + } +} +``` + +## Skeletonable + + Если необходимо изменить список конвертируемых в скелетоны view у какой-нибудь из отдельных view в иерархии, можно подписать его под протокол `Skeletonable` + +```swift +extension UITableViewCell: Skeletonable { + public var viewsToSkeletons: [UIView] { + contentView.subviews + } +} +``` + +## SkeletonsPresenter + + Чтобы не приходилось постоянно передавать в методы необходимые параметры для конфигурации можно соответствовать протоколу `SkeletonsPresenter`. Протокол дает возможность определять свойства для конфигурации скелетонов внутри view или viewController, вызывать метод `showSkeletons()` без передачи каких-либо параметров + + Перепишем _CanShowAndHideSkeletons_ под использование протокола + +```swift +class CanShowAndHideWithSkeletonsPresenter: CanShowAndHideSkeletons, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + SkeletonsConfiguration(skeletonsBackgroundColor: .gray) + } +} + +let canShowAndHideController = CanShowAndHideWithSkeletonsPresenter() +``` + +Skeletons will be shown with custom configuration + +```swift +canShowAndHideController.showSkeletons() +``` + +## Конфигурация внешнего вида + + Для конфигурации скелетонов существует класс `SkeletonsConfiguration` + +```swift +class CustomConfigurableSkeletonableView: UIView, SkeletonsPresenter { + var skeletonsConfiguration: SkeletonsConfiguration { + .init(skeletonsBackgroundColor: .blue) + } +} +``` + + Возможные опции для настройки: + + - анимация + - цвет + - форма + - отступы + + При этом все view делятся на: + - `UIView` с subviews (контейнеры) + - `UIView` без subviews + - `UILabel` + - `UITextView` + - `UIImageView` + + > Для контейнеров в качестве `borderColor` используется тот же цвет, что и для других скелетонов + +### Анимация + + `SkeletonsConfiguration` для настройки анимации принимает тип `(SkeletonsLayer) -> CAAnimationGroup`. Это означает, что при необходимости вы можете создать любую необходимую анимацию. Анимация будет применена к каждому скелетону. + + Однако для удобства существует уже определенный класс `SkeletonsAnimationBuilder` со статическим методом `createDirectionalGradientAnimation(_:)` для создания анимаций в одну из сторон: + + ```swift + public enum SkeletonsAnimationDirection { + case leftToRight + case rightToLeft + case topToBottom + case bottomToTop + case topLeftToBottomRight + case topRightToBottomLeft + case bottomLeftToTopRight + case bottomRightToTopLeft + } + ``` + +```swift +let confWithLeftToRightAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .leftToRight, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) + +let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in + let animConfig = DirectionalSkeletonsAnimationConfiguration(direction: .topToBottom, duration: 1.5) + return SkeletonsAnimationBuilder.createDirectionalGradientAnimation(animConfig) +}) +``` + +### Цвет + + За настройку цвета отвечает параметр `skeletonsBackgroundColor`: основной цвет скелетонов, им будт заливаться фон и выделяться _border_ + +```swift +let confWithRedBackgroundColor = SkeletonsConfiguration(skeletonsBackgroundColor: .red) +``` + +Для скрытия view от пользователя скелетонами накладывается специальный CALayer, чей цвет заполнения равен backgroundColor view, с которой были запущены скелетоны. Однако этот цвет можно переопределить переопределить с помощью параметра `baseSkeletonBackgroundColor` + +```swift +let confWithRedBaseBackgroundColor = SkeletonsConfiguration(baseSkeletonBackgroundColor: .red) +``` + +### Форма + + Форму можно настраивать отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, картинки можно сделать круглыми, а лейблы прямоугольные с закругленными краями: + +```swift +var confWithShape: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} +``` + +Для `UILabel` и `UITextView` есть возможность настроить высоту каждой строчки, расстояние между ними и их количество. + +```swift +var confWithLabelSettings: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(numberOfLines: 3, + lineHeight: { font in + if let font = font { + return font.pointSize + } + return 10 + + }, lineSpacing: { font in + if let font = font { + return font.xHeight + } + return 5 + }) + return .init(labelConfiguration: labelConf) +} +``` + +Для контейнеров можно настроить `borderWidth`. Стандартно он равняется 0, а значит контейнеры не будут показываться без дополнительной настройки ширины. + +```swift +var skeletonsConfiguration: SkeletonsConfiguration { + let containerConf = ContainerViewSkeletonsConfiguration(borderWidth: 4, + shape: .rectangle(cornerRadius: 10)) + + return .init(containerViewConfiguration: containerConf) +} +``` + +### Отступы + + Отступы можно настроить отдельно для `UILabel`, `UITextView`, `UIImageView` и остальных вью. Например, для предыдущего примера можно добавить горизонтальный _padding_ для лейбла: + +```swift +var confWithPadding: SkeletonsConfiguration { + let labelConf = TextSkeletonsConfiguration(padding: .horizontal(left: 15), shape: .rectangle(cornerRadius: 10)) + let imageConf = BaseViewSkeletonsConfiguration(shape: .circle) + + return .init(labelConfiguration: labelConf, + imageViewConfiguration: imageConf) +} +``` + +## Что если нужно больше? + + Если стандартной настройки не хватает, то в конфигуратор можно передать объект, соответствующий протоколу `SkeletonsConfigurationDelegate` через который можно настроить слой скелетона для каждой вью отдельно + + ```swift + public protocol SkeletonsConfigurationDelegate: AnyObject { + func layerDidConfigured(_ layer: SkeletonLayer, forViewType type: SkeletonLayer.ViewType) + } + ``` + +```swift +class SkeletonsConfDelegate: SkeletonsConfigurationDelegate { + func layerDidConfigured(_ layer: SkeletonLayer, forViewType type: SkeletonLayer.ViewType) { + if case .imageView(_) = type { + layer.frame = .init(x: layer.frame.minX - 20, + y: layer.frame.minY - 20, + width: layer.frame.width, + height: layer.frame.height) + } + } +} + +let delegate = SkeletonsConfDelegate() +let confWithDelegate = SkeletonsConfiguration(configurationDelegate: delegate) +``` + +### Особенности + + Т.к. размеры view на основе которой строятся скелетоны не модифицируются, может возникнуть ситуация, когда одни скелетоны перекрывают другие. Например, когда размер view меньше ее скелетонов. В таких случаях как раз может помочь установка позиции или размеров в методе делегата `SkeletonsConfigurationDelegate` + + Также в качестве способа обойти такую ситуацию можно передавать во view моковые данные для увеличения ее размеров, чтобы размеры были хотя бы примерно похожи на размер скелетонов, как в примере: + +```swift +extension DefaultTitleSubtitleView: SkeletonsPresenter { + public var skeletonsConfiguration: SkeletonsConfiguration { + .init(labelConfiguration: .init(numberOfLines: 3)) + } +} + +let titleSubtitleView = DefaultTitleSubtitleView() +titleSubtitleView.configure(appearance: .make { + $0.titleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } + $0.subtitleAppearance.update { + $0.textAttributes = .init(font: .systemFont(ofSize: 15), color: .black, alignment: .natural, isMultiline: true) + } +}) +titleSubtitleView.configure(with: .init(title: "very very long mock string to make multiple lines", + subtitle: "very very long mock string to make multiple lines")) + +titleSubtitleView.showSkeletons() + +DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { + titleSubtitleView.configure(with: .init(title: "normal data from a request", + subtitle: "normal data from a request")) + titleSubtitleView.hideSkeletons() +} +``` + +## Тестовый сконфигурированный контроллер + +```swift +import PlaygroundSupport + +canShowAndHideController.view.frame = .init(origin: .zero, size: .init(width: 250, height: 100)) + +canShowAndHideController.hideSkeletons() +confWithLeftToRightAnim.labelConfiguration = .init(numberOfLines: 2) + +PlaygroundPage.current.liveView = canShowAndHideController + +canShowAndHideController.showSkeletons(viewsToSkeletons: nil, confWithLeftToRightAnim) +``` diff --git a/project-scripts/bump_version.sh b/project-scripts/bump_version.sh index 71b79d5e..52b73451 100755 --- a/project-scripts/bump_version.sh +++ b/project-scripts/bump_version.sh @@ -1,15 +1,18 @@ #!/bin/sh -# Find source dir -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +# Description: +# Updates version in podspecs +# +# Parameters: +# $1 - new version +# +# Required environment variables: +# SRCROOT - path to project folder. +# +# Examples of usage: +# . bump_version.sh 1.34.1 +# -cd "$DIR" - -# Bump version -find ../ -name '*.podspec' \ - -not -path "../Carthage/*" \ - -not -path "../*/Carthage/*" \ - -not -path "../Pods/*" \ - -not -path "../*/Pods/*" \ - -not -path "../*/.gem/*" \ - | xargs -I% npx podspec-bump -i "$1" -w -p % +for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do + npx podspec-bump -i $1 -w -p ${SRCROOT}/${module_name}/${module_name}.podspec +done diff --git a/project-scripts/gen_docs_from_playgrounds.sh b/project-scripts/gen_docs_from_playgrounds.sh new file mode 100755 index 00000000..1c5e3242 --- /dev/null +++ b/project-scripts/gen_docs_from_playgrounds.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Description: +# Generates markdown documentation from nef playgrounds. +# +# Required environment variables: +# SRCROOT - path to project folder. +# + +PLAYGROUNDS="${SRCROOT}/TIFoundationUtils/TIFoundationUtils.app +${SRCROOT}/TIUIElements/TIUIElements.app" + +for playground_path in ${PLAYGROUNDS}; do + nef compile --project ${playground_path} + nef markdown --project ${playground_path} --output ${SRCROOT}/docs +done diff --git a/project-scripts/ordered_modules_list.txt b/project-scripts/ordered_modules_list.txt new file mode 100644 index 00000000..71f5d1ca --- /dev/null +++ b/project-scripts/ordered_modules_list.txt @@ -0,0 +1,19 @@ +TISwiftUtils +TIPagination +TIFoundationUtils +TIKeychainUtils +TIUIKitCore +TISwiftUICore +TIUIElements +TIAuth +TITableKitUtils +TINetworking +TINetworkingCache +TIMoyaNetworking +TIMapUtils +TIAppleMapUtils +TIGoogleMapUtils +TIYandexMapUtils +TIEcommerce +TIWebView +TIDeveloperUtils diff --git a/project-scripts/prepare_release.sh b/project-scripts/prepare_release.sh new file mode 100755 index 00000000..442154d1 --- /dev/null +++ b/project-scripts/prepare_release.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Description: +# Prepares new changes to release. +# +# Parameters: +# $1 - new version +# +# Required environment variables: +# SRCROOT - path to project folder. +# +# Examples of usage: +# . prepare_release.sh 1.34.1 +# + +. ${SRCROOT}/project-scripts/gen_docs_from_playgrounds.sh + +. ${SRCROOT}/project-scripts/bump_version.sh $1 \ No newline at end of file diff --git a/project-scripts/push_to_podspecs.sh b/project-scripts/push_to_podspecs.sh index 9dcf2183..b3a69986 100755 --- a/project-scripts/push_to_podspecs.sh +++ b/project-scripts/push_to_podspecs.sh @@ -1,30 +1,18 @@ #!/bin/sh -# Find source dir -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +# Description: +# Pushes podspec files to Podspecs repo +# +# Parameters: +# $1 - additional parameters for pod repo push command +# +# Required environment variables: +# SRCROOT - path to project folder. +# +# Examples of usage: +# SRCROOT=`pwd` ./project-scripts/push_to_podspecs.sh +# -cd "$DIR" - -ORDERED_PODSPECS="../TISwiftUtils/TISwiftUtils.podspec -../TIPagination/TIPagination.podspec -../TIFoundationUtils/TIFoundationUtils.podspec -../TIDeeplink/TIDeeplink.podspec -../TIKeychainUtils/TIKeychainUtils.podspec -../TIUIKitCore/TIUIKitCore.podspec -../TISwiftUICore/TISwiftUICore.podspec -../TIUIElements/TIUIElements.podspec -../TILogging/TILogging.podspec -../TIAuth/TIAuth.podspec -../TITableKitUtils/TITableKitUtils.podspec -../TINetworking/TINetworking.podspec -../TINetworkingCache/TINetworkingCache.podspec -../TIMoyaNetworking/TIMoyaNetworking.podspec -../TIMapUtils/TIMapUtils.podspec -../TIAppleMapUtils/TIAppleMapUtils.podspec -../TIGoogleMapUtils/TIGoogleMapUtils.podspec -../TIYandexMapUtils/TIYandexMapUtils.podspec -../TIEcommerce/TIEcommerce.podspec" - -for podspec_path in ${ORDERED_PODSPECS}; do - bundle exec pod repo push git@github.com:TouchInstinct/Podspecs ${podspec_path} --allow-warnings +for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do + bundle exec pod repo push git@gitlab.ti:touchinstinct/Podspecs ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings done diff --git a/setup b/setup index 97d7cd9d..4bdaeffc 100755 --- a/setup +++ b/setup @@ -3,5 +3,8 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$DIR" +# Set temporary environment variable +export SRCROOT=$DIR + # Configure githooks folder path -git config core.hooksPath .githooks \ No newline at end of file +git config core.hooksPath .githooks