From 9f5d7387d7889defd2bfc4eca988cfd173aadede Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 1 Mar 2023 19:05:52 +0300 Subject: [PATCH 1/4] feat: skeletons api --- CHANGELOG.md | 4 + TIAppleMapUtils/TIAppleMapUtils.podspec | 2 +- TIAuth/TIAuth.podspec | 2 +- TIDeveloperUtils/TIDeveloperUtils.podspec | 2 +- TIEcommerce/TIEcommerce.podspec | 2 +- TIFoundationUtils/TIFoundationUtils.podspec | 2 +- TIGoogleMapUtils/TIGoogleMapUtils.podspec | 2 +- TIKeychainUtils/TIKeychainUtils.podspec | 2 +- TILogging/TILogging.podspec | 2 +- TIMapUtils/TIMapUtils.podspec | 2 +- TIMoyaNetworking/TIMoyaNetworking.podspec | 2 +- TINetworking/TINetworking.podspec | 2 +- TINetworkingCache/TINetworkingCache.podspec | 2 +- TIPagination/TIPagination.podspec | 2 +- TISwiftUICore/TISwiftUICore.podspec | 2 +- TISwiftUtils/TISwiftUtils.podspec | 2 +- TITableKitUtils/TITableKitUtils.podspec | 2 +- .../BaseSkeletonsAnimationConfiguration.swift | 35 ++++ ...ionalSkeletonsAnimationConfiguration.swift | 37 ++++ .../Animation/SkeletonsAnimationBuilder.swift | 45 +++++ .../SkeletonsAnimationDirection.swift | 92 ++++++++++ .../BaseViewSkeletonsConfiguration.swift | 52 ++++++ .../LabelSkeletonsConfiguration.swift | 163 ++++++++++++++++++ .../SkeletonsConfiguration.swift | 94 ++++++++++ .../Skeletons/Helpers/CALayer+Skeletons.swift | 29 ++++ .../Skeletons/Helpers/UIView+Skeletons.swift | 84 +++++++++ .../Skeletons/Protocols/Skeletonable.swift | 27 +++ .../SkeletonsConfigurationDelegate.swift | 25 +++ .../Protocols/SkeletonsPresenter.swift | 154 +++++++++++++++++ .../Views/Skeletons/SkeletonLayer.swift | 159 +++++++++++++++++ TIUIElements/TIUIElements.podspec | 2 +- TIUIKitCore/TIUIKitCore.podspec | 2 +- TIWebView/TIWebView.podspec | 2 +- TIYandexMapUtils/TIYandexMapUtils.podspec | 2 +- setup | 5 +- 35 files changed, 1024 insertions(+), 21 deletions(-) create mode 100644 TIUIElements/Sources/Views/Skeletons/Animation/BaseSkeletonsAnimationConfiguration.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationBuilder.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Animation/SkeletonsAnimationDirection.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Configuration/LabelSkeletonsConfiguration.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Helpers/CALayer+Skeletons.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Helpers/UIView+Skeletons.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Protocols/Skeletonable.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsConfigurationDelegate.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift create mode 100644 TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 939213b8..5c3fc4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 1.36.0 + +- **Added**: API for converting view hierarchy to skeletons + ### 1.35.1 - **Added**: Auto documentation generation for `TIFoundationUtils` playground and compile checks for playground before release diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 339d3b27..dc80fcdc 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index ae8f0d87..8b7fcfec 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 11469088..a2580e31 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Universal web view API' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index 7e1ccab1..a7adf60b 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index f31f6a4a..3d89b8f3 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index f19719fc..31195b66 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index fc391f7e..57162d4a 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index 2d6ade90..d5070666 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Logging API' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index cc1a494e..5819982a 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 52903c66..46259090 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 19417603..0e403b87 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 3073835f..3a151388 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index 7dcbb29e..ffcc30e5 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Generic pagination component.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 267918bc..6a2a4a42 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 8774d395..f927fd06 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index 60df6488..cf268632 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } 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..d07616fd --- /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, + 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..935a4069 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift @@ -0,0 +1,52 @@ +// +// 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 shape: Shape + + public init(shape: Shape = .rectangle(cornerRadius: .zero)) { + self.shape = shape + } + + open func drawPath(rect: CGRect) -> CGPath { + switch shape { + case let .custom(path): + return path + + case let .rectangle(cornerRadius: cornerRadius): + let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius) + return path.cgPath + + case .circle: + return CGPath(ellipseIn: rect, transform: nil) + } + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/LabelSkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/LabelSkeletonsConfiguration.swift new file mode 100644 index 00000000..602e4086 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/LabelSkeletonsConfiguration.swift @@ -0,0 +1,163 @@ +// +// 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 LabelSkeletonsConfiguration: BaseViewSkeletonsConfiguration { + + public enum LinesAmount { + case constant(Int) + case asLabelSize + case asLabelNumberOfLines + } + + private var isMultiline = false + private var labelNumberOfLines: Int = .zero + private var labelHeight: CGFloat = .zero + private var font: UIFont? + + public var numberOfLines: LinesAmount + public var lineHeight: Closure? + public var lineSpacing: Closure? + + public init(numberOfLines: LinesAmount = .constant(3), + lineHeight: Closure? = nil, + lineSpacing: Closure? = nil, + shape: Shape = .rectangle(cornerRadius: .zero)) { + + self.numberOfLines = numberOfLines + self.lineHeight = lineHeight + self.lineSpacing = lineSpacing + + super.init(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) + } + + 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) + } + + // 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 + } + + switch self.numberOfLines { + case let .constant(lines): + return lines + + case .asLabelNumberOfLines: + return labelNumberOfLines + + case .asLabelSize: + let lineHeight = getLineHeight() + return Int(labelHeight / lineHeight) + } + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift new file mode 100644 index 00000000..e6ed3979 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift @@ -0,0 +1,94 @@ +// +// 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 labelConfiguration: LabelSkeletonsConfiguration + public var imageViewConfiguration: BaseViewSkeletonsConfiguration + public var animation: ResultClosure? + + public var skeletonsBackgroundColor: CGColor + public var skeletonsMovingColor: CGColor + public var borderWidth: CGFloat + + public weak var configurationDelegate: SkeletonsConfigurationDelegate? + + public var isContainersHidden: Bool { + borderWidth == .zero + } + + // MARK: - Init + + public init(viewConfiguration: BaseViewSkeletonsConfiguration = .init(), + labelConfiguration: LabelSkeletonsConfiguration = .init(), + imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(), + animation: ResultClosure? = nil, + skeletonsBackgroundColor: UIColor = .lightGray.withAlphaComponent(0.7), + skeletonsMovingColor: UIColor = .lightGray.withAlphaComponent(0.2), + borderWidth: CGFloat = .zero, + configurationDelegate: SkeletonsConfigurationDelegate? = nil) { + + self.viewConfiguration = viewConfiguration + self.labelConfiguration = labelConfiguration + self.imageViewConfiguration = imageViewConfiguration + self.animation = animation + self.skeletonsBackgroundColor = skeletonsBackgroundColor.cgColor + self.skeletonsMovingColor = skeletonsMovingColor.cgColor + self.borderWidth = borderWidth + 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 configureContainerAppearance(layer: SkeletonLayer) { + layer.fillColor = UIColor.clear.cgColor + + if !isContainersHidden { + layer.borderColor = skeletonsBackgroundColor + layer.borderWidth = borderWidth + + if case let .rectangle(cornerRadius: radius) = viewConfiguration.shape { + layer.cornerRadius = radius + } + } + } + + open func configureAppearance(gradientLayer: CAGradientLayer) { + gradientLayer.colors = [ + skeletonsBackgroundColor, + skeletonsMovingColor, + skeletonsBackgroundColor, + ] + } +} 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/UIView+Skeletons.swift b/TIUIElements/Sources/Views/Skeletons/Helpers/UIView+Skeletons.swift new file mode 100644 index 00000000..dd6da0eb --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Helpers/UIView+Skeletons.swift @@ -0,0 +1,84 @@ +// +// 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 UIView { + public var skeletonableViews: [UIView] { + if let skeletonableView = self as? Skeletonable { + return skeletonableView.viewsToSkeletone + } + + return subviews + } + + var isSkeletonsContainer: Bool { + if let skeletonableView = self as? Skeletonable { + return !skeletonableView.viewsToSkeletone.isEmpty + } + + return !subviews.isEmpty + } + + var viewType: SkeletonLayer.ViewType { + if let labelView = self as? UILabel { + return .label(labelView) + + } else if let textView = self as? UITextView { + return .textView(textView) + + } else if let imageView = self as? UIImageView { + return .imageView(imageView) + + } else if self.isSkeletonsContainer { + return .container(self) + + } else { + return .generic(self) + } + } +} + +extension UITextView { + var isMultiline: Bool { + guard let text = text, let font = font else { + return false + } + + let labelTextSize = (text as NSString).size(withAttributes: [.font: font]) + + return labelTextSize.width > bounds.width + } +} + +extension UILabel { + var isMultiline: Bool { + // Unwrapping font to mute worning while casting UIFont! to Any + guard let text = text, let font = font else { + return false + } + + let labelTextSize = (text as NSString).size(withAttributes: [.font: font]) + + return labelTextSize.width > bounds.width + } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Protocols/Skeletonable.swift b/TIUIElements/Sources/Views/Skeletons/Protocols/Skeletonable.swift new file mode 100644 index 00000000..8b8020b2 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Protocols/Skeletonable.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 class UIKit.UIView + +public protocol Skeletonable { + var viewsToSkeletone: [UIView] { get } +} diff --git a/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsConfigurationDelegate.swift b/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsConfigurationDelegate.swift new file mode 100644 index 00000000..7caf34fc --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsConfigurationDelegate.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public protocol SkeletonsConfigurationDelegate: AnyObject { + func layerDidConfigured(forViewType type: SkeletonLayer.ViewType, layer: SkeletonLayer) +} diff --git a/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift b/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift new file mode 100644 index 00000000..c3946379 --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/Protocols/SkeletonsPresenter.swift @@ -0,0 +1,154 @@ +// +// 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 SkeletonsPresenter { + var baseView: UIView? { get } + var skeletonsConfiguration: SkeletonsConfiguration { get } + var isSkeletonsHidden: Bool { get } + var viewsToSkeletone: [UIView] { get } + + func showSkeletons() + func hideSkeletons() + func startAnimation() + func stopAnimation() +} + +// MARK: - SkeletonsPresenter + Default implemetation + +extension SkeletonsPresenter { + public func showSkeletons() { + guard let baseView = baseView else { + return + } + + baseView.isUserInteractionEnabled = false + + viewsToSkeletone + .flatMap { view in + view.isHidden = true + + return getSkeletonLayer(forView: view) + } + .map { layer in + layer.startAnimation() + + return layer + } + .insert(onto: baseView) + } + + public func hideSkeletons() { + guard let baseView = baseView else { + return + } + + baseView.isUserInteractionEnabled = true + + baseView.layer.sublayers?.forEach { layer in + if let layer = layer as? SkeletonLayer { + layer.remove(from: baseView) + } + } + + baseView.skeletonableViews.forEach { + $0.isHidden = false + } + } + + public func startAnimation() { + baseView?.layer.skeletonLayers.forEach { layer in + layer.startAnimation() + } + } + + public func stopAnimation() { + baseView?.layer.skeletonLayers.forEach { layer in + layer.stopAnimation() + } + } + + // MARK: - Private methods + + private func getSkeletonLayer(forView view: UIView) -> [SkeletonLayer] { + let skeletonLayer = skeletonsConfiguration.createSkeletonLayer(for: baseView) + var subviewSkeletonLayers = [SkeletonLayer]() + + if view.isSkeletonsContainer { + if skeletonsConfiguration.borderWidth != .zero { + skeletonLayer.bind(to: .container(view)) + } + + subviewSkeletonLayers = view.skeletonableViews + .map(getSkeletonLayer(forView:)) + .flatMap { $0 } + + } else { + skeletonLayer.bind(to: view.viewType) + } + + return [skeletonLayer] + subviewSkeletonLayers + } +} + +// MARK: - UIView + SkeletonsPresenter + +extension SkeletonsPresenter where Self: UIView { + public var baseView: UIView? { + self + } + + public var isSkeletonsHidden: Bool { + (layer.sublayers ?? []).first { $0 is SkeletonLayer } == nil + } + + public var viewsToSkeletone: [UIView] { + skeletonableViews + } +} + +// MARK: - UIViewController + SkeletonsPresenter + +extension SkeletonsPresenter where Self: UIViewController { + public var baseView: UIView? { + view + } + + public var isSkeletonsHidden: Bool { + (view.layer.sublayers ?? []).first { $0 is SkeletonLayer } == nil + } + + public var viewsToSkeletone: [UIView] { + baseView?.skeletonableViews ?? view.skeletonableViews + } +} + +// 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/SkeletonLayer.swift b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift new file mode 100644 index 00000000..30b2b88b --- /dev/null +++ b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift @@ -0,0 +1,159 @@ +// +// 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 generic(UIView) + case container(UIView) + case label(UILabel) + case textView(UITextView) + case imageView(UIImageView) + + public var view: UIView { + switch self { + case let .imageView(imageView): + return imageView + + case let .container(containerView): + return containerView + + case let .label(labelView): + return labelView + + case let .textView(textView): + return textView + + case let .generic(view): + return view + } + } + } + + private var animationLayer = CAGradientLayer() + private var viewBoundsObservation: NSKeyValueObservation? + + public var configuration: SkeletonsConfiguration + public weak var baseView: UIView? + + // 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 + view.isHidden = true + self?.updateGeometry(viewType: view.viewType) + } + + configuration.configurationDelegate?.layerDidConfigured(forViewType: viewType, layer: self) + } + + open func remove(from view: UIView) { + removeFromSuperlayer() + } + + open func startAnimation() { + guard let animation = configuration.animation?() else { + return + } + + animationLayer.add(animation, forKey: Constants.animationKeyPath) + mask = animationLayer + } + + open func stopAnimation() { + animationLayer.removeAllAnimations() + mask = nil + } + + // MARK: - Private methods + + private func configureAppearance(_ type: ViewType) { + switch type { + case .container(_): + configuration.configureContainerAppearance(layer: self) + + default: + configuration.configureAppearance(layer: self) + } + + configuration.configureAppearance(gradientLayer: animationLayer) + } + + private func updateGeometry(viewType: ViewType) { + frame = viewType.view.convert(viewType.view.bounds, to: baseView) + path = drawPath(viewType: viewType) + animationLayer.frame = bounds + } + + private func drawPath(viewType: ViewType) -> CGPath { + var path: CGPath + + switch viewType { + case let .textView(textView): + path = configuration.labelConfiguration.configureTextViewPath(textView: textView) + + case let .label(label): + path = configuration.labelConfiguration.configureLabelPath(label: label) + + case .imageView(_): + path = configuration.imageViewConfiguration.drawPath(rect: viewType.view.bounds) + + default: + path = configuration.viewConfiguration.drawPath(rect: viewType.view.bounds) + } + + return path + } +} diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 336d94b3..b2e04081 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 4e3768de..e8bededa 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index a63876cc..c6c2afeb 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.35.1' + s.version = '1.36.0' s.summary = 'Universal web view API' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index ec22d532..b3d899dc 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.35.1' + s.version = '1.36.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.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/setup b/setup index 97d7cd9d..88c536ef 100755 --- a/setup +++ b/setup @@ -3,5 +3,8 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$DIR" +# Set temporary environment variable +export SRCROOT=. + # Configure githooks folder path -git config core.hooksPath .githooks \ No newline at end of file +git config core.hooksPath .githooks From 6e506aa385e82c88941e700ad78b6ac7fb0f51f4 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 1 Mar 2023 19:16:33 +0300 Subject: [PATCH 2/4] feat: updated exporting of environment variable --- setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup b/setup index 88c536ef..4bdaeffc 100755 --- a/setup +++ b/setup @@ -4,7 +4,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$DIR" # Set temporary environment variable -export SRCROOT=. +export SRCROOT=$DIR # Configure githooks folder path git config core.hooksPath .githooks From 7442884856b40da7165e370d1d018050489fd68b Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 1 Mar 2023 20:39:04 +0300 Subject: [PATCH 3/4] fix: minor changes --- .../DirectionalSkeletonsAnimationConfiguration.swift | 2 +- .../Skeletons/Configuration/SkeletonsConfiguration.swift | 9 ++++----- TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift | 6 ++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift index d07616fd..5ecbc21e 100644 --- a/TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift +++ b/TIUIElements/Sources/Views/Skeletons/Animation/DirectionalSkeletonsAnimationConfiguration.swift @@ -27,7 +27,7 @@ open class DirectionalSkeletonsAnimationConfiguration: BaseSkeletonsAnimationCon public var direction: SkeletonsAnimationDirection public init(direction: SkeletonsAnimationDirection = .leftToRight, - duration: CFTimeInterval = 1, + duration: CFTimeInterval = 1.5, timingFunction: CAMediaTimingFunction? = nil) { self.direction = direction diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift index e6ed3979..1c37de3f 100644 --- a/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift @@ -28,7 +28,7 @@ open class SkeletonsConfiguration { public var viewConfiguration: BaseViewSkeletonsConfiguration public var labelConfiguration: LabelSkeletonsConfiguration public var imageViewConfiguration: BaseViewSkeletonsConfiguration - public var animation: ResultClosure? + public var animation: Closure? public var skeletonsBackgroundColor: CGColor public var skeletonsMovingColor: CGColor @@ -36,7 +36,7 @@ open class SkeletonsConfiguration { public weak var configurationDelegate: SkeletonsConfigurationDelegate? - public var isContainersHidden: Bool { + open var isContainersHidden: Bool { borderWidth == .zero } @@ -45,9 +45,8 @@ open class SkeletonsConfiguration { public init(viewConfiguration: BaseViewSkeletonsConfiguration = .init(), labelConfiguration: LabelSkeletonsConfiguration = .init(), imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(), - animation: ResultClosure? = nil, + animation: Closure? = nil, skeletonsBackgroundColor: UIColor = .lightGray.withAlphaComponent(0.7), - skeletonsMovingColor: UIColor = .lightGray.withAlphaComponent(0.2), borderWidth: CGFloat = .zero, configurationDelegate: SkeletonsConfigurationDelegate? = nil) { @@ -56,7 +55,7 @@ open class SkeletonsConfiguration { self.imageViewConfiguration = imageViewConfiguration self.animation = animation self.skeletonsBackgroundColor = skeletonsBackgroundColor.cgColor - self.skeletonsMovingColor = skeletonsMovingColor.cgColor + self.skeletonsMovingColor = skeletonsBackgroundColor.withAlphaComponent(0.2).cgColor self.borderWidth = borderWidth self.configurationDelegate = configurationDelegate } diff --git a/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift index 30b2b88b..8c213cbb 100644 --- a/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift +++ b/TIUIElements/Sources/Views/Skeletons/SkeletonLayer.swift @@ -100,11 +100,13 @@ open class SkeletonLayer: CAShapeLayer { } open func remove(from view: UIView) { + stopAnimation() removeFromSuperlayer() + viewBoundsObservation = nil } open func startAnimation() { - guard let animation = configuration.animation?() else { + guard let animation = configuration.animation?(self) else { return } @@ -114,7 +116,7 @@ open class SkeletonLayer: CAShapeLayer { open func stopAnimation() { animationLayer.removeAllAnimations() - mask = nil + mask?.removeFromSuperlayer() } // MARK: - Private methods From 59ef1093c789f375915e57267cd6b2669eed2072 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 1 Mar 2023 20:40:33 +0300 Subject: [PATCH 4/4] fix: change default shape of UIImageViews --- .../Views/Skeletons/Configuration/SkeletonsConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift index 1c37de3f..78a12f4a 100644 --- a/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/SkeletonsConfiguration.swift @@ -44,7 +44,7 @@ open class SkeletonsConfiguration { public init(viewConfiguration: BaseViewSkeletonsConfiguration = .init(), labelConfiguration: LabelSkeletonsConfiguration = .init(), - imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(), + imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(shape: .circle), animation: Closure? = nil, skeletonsBackgroundColor: UIColor = .lightGray.withAlphaComponent(0.7), borderWidth: CGFloat = .zero,