HolderViewSkeletonsConfiguration, CALayer support for DashedBoundsLayer #29

Merged
ivan.smolin merged 1 commits from feature/skeletons_holder_configuration into master 2024-02-12 10:52:51 +03:00
39 changed files with 410 additions and 167 deletions

View File

@ -1,5 +1,10 @@
# Changelog
### 1.56.0
- **Update**: `ViewSkeletonsConfiguration`. It's possible to enable or disable animation for specific skeletons now.
- **Added**: `HolderViewSkeletonsConfiguration` for skeleton root view configuration
- **Added**: `DashedBoundsLayer` can now be applied to `CALayer`
### 1.55.1
- **Update**: revert `TextSkeletonsConfiguration` line height calculation

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAppleMapUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIApplication'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Application architecture.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAuth'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Login, registration, confirmation and other related actions'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIBottomSheet'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Base models for creating bottom sheet view controllers'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TICoreGraphicsUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'CoreGraphics drawing helpers'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIDeeplink'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Deeplink service API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -35,82 +35,26 @@ open class DashedBoundsLayer: CAShapeLayer {
// MARK: - Open methods
open func configure(on view: UIView) {
configure(on: view.layer)
}
open func configure(on layer: CALayer) {
fillColor = UIColor.clear.cgColor
strokeColor = dashColor.cgColor
lineWidth = 1
lineDashPattern = [4.0, 2.0]
updateGeometry(from: view)
updateGeometry(newBounds: layer.bounds)
view.layer.addSublayer(self)
layer.addSublayer(self)
viewBoundsObservation = view.observe(\.bounds, options: [.new]) { [weak self] view, _ in
self?.updateGeometry(from: view)
viewBoundsObservation = layer.observe(\.bounds, options: [.new]) { [weak self] layer, _ in
self?.updateGeometry(newBounds: layer.bounds)
}
}
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, after delay: DispatchTimeInterval? = nil) -> UIView {
guard let delay else {
debugBoundsVisually(debugSubviews: debugSubviews)
return self
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.debugBoundsVisually(debugSubviews: debugSubviews)
}
return self
}
@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,
after delay: DispatchTimeInterval? = nil) -> UIViewController {
view.debugBoundsVisually(debugSubviews: debugSubviews, after: delay)
return self
open func updateGeometry(newBounds: CGRect) {
frame = newBounds
path = UIBezierPath(rect: newBounds).cgPath
}
}

View File

@ -0,0 +1,69 @@
//
// Copyright (c) 2024 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 CALayer {
@discardableResult
func debugBoundsVisually(debugSublayers: Bool = true,
after delay: DispatchTimeInterval? = nil,
filterSublayers isIncluded: @escaping (CALayer) -> Bool = { _ in true }) -> Self {
guard let delay else {
debugBoundsVisually(debugSublayers: debugSublayers, filterSublayers: isIncluded)
return self
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.debugBoundsVisually(debugSublayers: debugSublayers, filterSublayers: isIncluded)
}
return self
}
@discardableResult
func debugBoundsVisually(debugSublayers: Bool = true,
filterSublayers isIncluded: (CALayer) -> Bool = { _ in true }) -> Self {
disableBoundsVisuallyDebug()
if debugSublayers {
for sublayer in sublayers?.filter(isIncluded) ?? [] {
sublayer.debugBoundsVisually(debugSublayers: debugSublayers)
}
}
let dashedLayer = DashedBoundsLayer()
dashedLayer.configure(on: self)
return self
}
func disableBoundsVisuallyDebug() {
for sublayer in sublayers ?? [] {
if sublayer is DashedBoundsLayer {
sublayer.removeFromSuperlayer()
} else {
sublayer.disableBoundsVisuallyDebug()
}
}
}
}

View File

@ -0,0 +1,71 @@
//
// Copyright (c) 2024 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 UIView {
@discardableResult
func debugBoundsVisually(debugSubviews: Bool = true,
after delay: DispatchTimeInterval? = nil,
filterSubviews isIncluded: @escaping (UIView) -> Bool = { _ in true }) -> Self {
guard let delay else {
debugBoundsVisually(debugSubviews: debugSubviews)
return self
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.debugBoundsVisually(debugSubviews: debugSubviews)
}
return self
}
@discardableResult
func debugBoundsVisually(debugSubviews: Bool = true,
filterSubviews isIncluded: @escaping (UIView) -> Bool = { _ in true }) -> Self {
disableBoundsVisuallyDebug()
if debugSubviews {
for subview in subviews.filter(isIncluded) {
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()
}
}
}

View File

@ -0,0 +1,37 @@
//
// Copyright (c) 2024 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 UIViewController {
@discardableResult
func debugBoundsVisually(debugSubviews: Bool = true,
filterSubviews isIncluded: @escaping (UIView) -> Bool = { _ in true },
after delay: DispatchTimeInterval? = nil) -> Self {
view.debugBoundsVisually(debugSubviews: debugSubviews,
after: delay,
filterSubviews: isIncluded)
return self
}
}

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIDeveloperUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Universal web view API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIEcommerce'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Cart, products, promocodes, bonuses and other related actions'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIFoundationUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Set of helpers for Foundation framework classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIGoogleMapUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIKeychainUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Set of helpers for Keychain classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TILogging'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Logging for TI libraries.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMapUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Set of helpers for map objects clustering and interacting.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMoyaNetworking'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Moya + Swagger network service.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworking'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Swagger-frendly networking layer helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworkingCache'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Caching results of EndpointRequests.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIPagination'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Generic pagination component.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUICore'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Bunch of useful helpers for Swift development.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITableKitUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Set of helpers for TableKit classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITextProcessing'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -26,9 +26,13 @@ import UIKit
open class BaseViewSkeletonsConfiguration {
open class Defaults {
public static var cornerRadius: CGFloat {
public class var cornerRadius: CGFloat {
16
}
public class var animatable: Bool {
true
}
}
public enum Shape {
@ -40,14 +44,17 @@ open class BaseViewSkeletonsConfiguration {
public var padding: UIEdgeInsets
public var maxWidth: CGFloat
public var shape: Shape
public var animatable: Bool
public init(padding: UIEdgeInsets = .edges(5),
maxWidth: CGFloat = .infinity,
shape: Shape = .rectangle(cornerRadius: Defaults.cornerRadius)) {
shape: Shape = .rectangle(cornerRadius: Defaults.cornerRadius),
animatable: Bool = Defaults.animatable) {
self.shape = shape
self.maxWidth = maxWidth
self.padding = padding
self.animatable = animatable
}
open func createPath(for rect: CGRect) -> CGPath {
@ -74,10 +81,12 @@ open class BaseViewSkeletonsConfiguration {
open func copyWith(padding: UIEdgeInsets? = nil,
maxWidth: CGFloat? = nil,
shape: Shape? = nil) -> BaseViewSkeletonsConfiguration {
shape: Shape? = nil,
animatabel: Bool? = nil) -> BaseViewSkeletonsConfiguration {
BaseViewSkeletonsConfiguration(padding: padding ?? self.padding,
maxWidth: maxWidth ?? self.maxWidth,
shape: shape ?? self.shape)
shape: shape ?? self.shape,
animatable: animatabel ?? self.animatable)
}
}

View File

@ -25,31 +25,41 @@ import UIKit
open class ContainerViewSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
open class Defaults: BaseViewSkeletonsConfiguration.Defaults {
public static var borderWidth: CGFloat {
public class var borderWidth: CGFloat {
1
}
}
public var borderWidth: CGFloat
public var isContainerHidden: Bool {
borderWidth == .zero
}
public init(borderWidth: CGFloat = Defaults.borderWidth,
padding: UIEdgeInsets = .zero,
maxWidth: CGFloat = .infinity,
shape: Shape = .rectangle(cornerRadius: Defaults.cornerRadius)) {
shape: Shape = .rectangle(cornerRadius: Defaults.cornerRadius),
animatable: Bool = Defaults.animatable) {
self.borderWidth = borderWidth
super.init(padding: padding, shape: shape)
super.init(padding: padding,
maxWidth: maxWidth,
shape: shape,
animatable: animatable)
}
open func copyWith(borderWidth: CGFloat? = nil,
padding: UIEdgeInsets? = nil,
maxWidth: CGFloat? = nil,
shape: Shape? = nil) -> ContainerViewSkeletonsConfiguration {
shape: Shape? = nil,
animatable: Bool? = nil) -> ContainerViewSkeletonsConfiguration {
ContainerViewSkeletonsConfiguration(borderWidth: borderWidth ?? self.borderWidth,
padding: padding ?? self.padding,
maxWidth: maxWidth ?? self.maxWidth,
shape: shape ?? self.shape)
shape: shape ?? self.shape,
animatable: animatable ?? self.animatable)
}
}

View File

@ -0,0 +1,77 @@
//
// Copyright (c) 2024 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 HolderViewSkeletonsConfiguration: ContainerViewSkeletonsConfiguration {
open class Defaults: ContainerViewSkeletonsConfiguration.Defaults {
public override class var borderWidth: CGFloat {
.zero
}
public class var backgroundColor: UIColor? {
nil
}
public override class var cornerRadius: CGFloat {
.zero
}
public override class var animatable: Bool {
false
}
}
public var backgroundColor: UIColor?
public init(backgroundColor: UIColor? = Defaults.backgroundColor,
borderWidth: CGFloat = Defaults.borderWidth,
padding: UIEdgeInsets = .zero,
maxWidth: CGFloat = .infinity,
shape: Shape = .rectangle(cornerRadius: Defaults.cornerRadius),
animatable: Bool = Defaults.animatable) {
self.backgroundColor = backgroundColor
super.init(borderWidth: borderWidth,
padding: padding,
maxWidth: maxWidth,
shape: shape,
animatable: animatable)
}
open func copyWith(backgroundColor: UIColor? = nil,
borderWidth: CGFloat? = nil,
padding: UIEdgeInsets? = nil,
maxWidth: CGFloat? = nil,
shape: Shape? = nil,
animatable: Bool? = nil) -> HolderViewSkeletonsConfiguration {
HolderViewSkeletonsConfiguration(backgroundColor: backgroundColor ?? self.backgroundColor,
borderWidth: borderWidth ?? self.borderWidth,
padding: padding ?? self.padding,
maxWidth: maxWidth ?? self.maxWidth,
shape: shape ?? self.shape,
animatable: animatable ?? self.animatable)
}
}

View File

@ -36,37 +36,33 @@ open class SkeletonsConfiguration {
public var viewConfiguration: BaseViewSkeletonsConfiguration
public var containerViewConfiguration: ContainerViewSkeletonsConfiguration
public var holderViewConfiguration: HolderViewSkeletonsConfiguration
public var labelConfiguration: TextSkeletonsConfiguration
public var imageViewConfiguration: BaseViewSkeletonsConfiguration
public var animation: Closure<SkeletonLayer, CAAnimationGroup>?
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(),
holderViewConfiguration: HolderViewSkeletonsConfiguration = .init(),
labelConfiguration: TextSkeletonsConfiguration = .init(),
imageViewConfiguration: BaseViewSkeletonsConfiguration = .init(shape: .circle),
animation: Closure<SkeletonLayer, CAAnimationGroup>? = Defaults.animation,
baseSkeletonBackgroundColor: UIColor? = nil,
skeletonsBackgroundColor: UIColor = .lightGray.withAlphaComponent(0.7),
configurationDelegate: SkeletonsConfigurationDelegate? = nil) {
self.viewConfiguration = viewConfiguration
self.containerViewConfiguration = containerViewConfiguration
self.holderViewConfiguration = holderViewConfiguration
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
@ -82,21 +78,18 @@ open class SkeletonsConfiguration {
layer.fillColor = skeletonsBackgroundColor
}
open func configureBaseViewAppearance(layer: SkeletonLayer, view: UIView) {
layer.fillColor = baseSkeletonBackgroundColor ?? view.backgroundColor?.cgColor
open func configureHolderViewAppearance(layer: SkeletonLayer, view: UIView) {
layer.fillColor = holderViewConfiguration.backgroundColor?.cgColor ?? view.backgroundColor?.cgColor
configureApparance(of: layer,
with: holderViewConfiguration)
}
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
}
}
configureApparance(of: layer,
with: containerViewConfiguration)
}
open func configureAppearance(gradientLayer: CAGradientLayer) {
@ -106,4 +99,17 @@ open class SkeletonsConfiguration {
skeletonsBackgroundColor,
]
}
private func configureApparance(of layer: SkeletonLayer,
with configuration: some ContainerViewSkeletonsConfiguration) {
if !configuration.isContainerHidden {
layer.borderColor = skeletonsBackgroundColor
layer.borderWidth = containerViewConfiguration.borderWidth
if case let .rectangle(cornerRadius: radius) = configuration.shape {
layer.cornerRadius = radius
}
}
}
}

View File

@ -27,19 +27,19 @@ import UIKit
open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
open class Defaults: BaseViewSkeletonsConfiguration.Defaults {
public static var numberOfLines: Int {
public class var numberOfLines: Int {
1
}
public static var multilineNumberOfLines: Int {
public class var multilineNumberOfLines: Int {
3
}
public static var linesWidthFraction: [CGFloat] {
public class var linesWidthFraction: [CGFloat] {
[1, 0.65, 0.78]
}
public static var defaultFont: UIFont {
public class var defaultFont: UIFont {
.systemFont(ofSize: 15)
}
}
@ -55,14 +55,15 @@ open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
lineSpacing: CGFloat? = nil,
padding: UIEdgeInsets = .edges(5),
maxWidth: CGFloat = .infinity,
shape: Shape = .rectangle(cornerRadius: Defaults.cornerRadius)) {
shape: Shape = .rectangle(cornerRadius: Defaults.cornerRadius),
animatable: Bool = Defaults.animatable) {
self.numberOfLines = numberOfLines
self.linesWidthFraction = linesWidthFraction
self.lineHeight = lineHeight
self.lineSpacing = lineSpacing
super.init(padding: padding, shape: shape)
super.init(padding: padding, shape: shape, animatable: animatable)
}
open func createConfiguration(for label: UILabel) -> TextSkeletonsConfiguration {
@ -73,7 +74,8 @@ open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
lineSpacing: calculateLineSpacing(for: labelFont),
padding: padding,
maxWidth: maxWidth,
shape: shape)
shape: shape,
animatable: animatable)
}
open func createConfiguration(for textView: UITextView) -> TextSkeletonsConfiguration {
@ -84,7 +86,8 @@ open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
lineSpacing: calculateLineSpacing(for: labelFont),
padding: padding,
maxWidth: maxWidth,
shape: shape)
shape: shape,
animatable: animatable)
}
open override func createPath(for rect: CGRect) -> CGPath {
@ -192,7 +195,8 @@ open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
lineSpacing: CGFloat? = nil,
padding: UIEdgeInsets? = nil,
maxWidth: CGFloat? = nil,
shape: Shape? = nil) -> TextSkeletonsConfiguration {
shape: Shape? = nil,
animatable: Bool? = nil) -> TextSkeletonsConfiguration {
TextSkeletonsConfiguration(numberOfLines: numberOfLines ?? self.numberOfLines,
linesWidthFraction: linesWidthFraction ?? self.linesWidthFraction,
@ -200,7 +204,8 @@ open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
lineSpacing: lineSpacing ?? self.lineSpacing,
padding: padding ?? self.padding,
maxWidth: maxWidth ?? self.maxWidth,
shape: shape ?? self.shape)
shape: shape ?? self.shape,
animatable: animatable ?? self.animatable)
}
private func getLinesCount(viewNumberOfLines: Int) -> Int {

View File

@ -65,11 +65,12 @@ open class SkeletonLayer: CAShapeLayer {
private var geometryChangeObservation: Invalidatable?
private var applicationStateObservation: NSObjectProtocol?
private var animatable = BaseViewSkeletonsConfiguration.Defaults.animatable
private weak var layerOwnerView: UIView?
private weak var skeletonsPresenterView: UIView?
public var configuration: SkeletonsConfiguration
public var isSkeletonsHolder: Bool = false
public var isAnimating: Bool {
animationLayer.animation(forKey: Defaults.animationKeyPath) != nil
@ -151,7 +152,7 @@ open class SkeletonLayer: CAShapeLayer {
open func startAnimation() {
guard !isAnimating,
!isSkeletonsHolder,
animatable,
let animation = configuration.animation?(self) else {
return
}
@ -173,8 +174,7 @@ open class SkeletonLayer: CAShapeLayer {
configuration.configureContainerAppearance(layer: self)
case let .skeletonsHolderView(view):
isSkeletonsHolder = true
configuration.configureBaseViewAppearance(layer: self, view: view)
configuration.configureHolderViewAppearance(layer: self, view: view)
default:
configuration.configureAppearance(layer: self)
@ -191,42 +191,51 @@ open class SkeletonLayer: CAShapeLayer {
case let .textView(textView):
let textViewConfig = configuration.labelConfiguration.createConfiguration(for: textView)
path = textViewConfig.createPath(for: textView.bounds)
let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size)
frame = textViewConfig.applyPadding(viewFrame: viewFrame)
updateGeometry(rect: rect,
view: textView,
configuration: textViewConfig)
case let .label(label):
let labelConfig = configuration.labelConfiguration.createConfiguration(for: label)
path = labelConfig.createPath(for: label.bounds)
let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size)
frame = labelConfig.applyPadding(viewFrame: viewFrame)
updateGeometry(rect: rect,
view: label,
configuration: labelConfig)
case .imageView(_):
path = configuration.imageViewConfiguration.createPath(for: viewType.view.bounds)
let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size)
frame = configuration.imageViewConfiguration.applyPadding(viewFrame: viewFrame)
updateGeometry(rect: rect,
view: view,
configuration: configuration.imageViewConfiguration)
case .parentView(_):
path = configuration.containerViewConfiguration.createPath(for: viewType.view.bounds)
let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size)
frame = configuration.containerViewConfiguration.applyPadding(viewFrame: viewFrame)
updateGeometry(rect: rect,
view: view,
configuration: configuration.containerViewConfiguration)
case .leafView(_):
path = configuration.viewConfiguration.createPath(for: viewType.view.bounds)
updateGeometry(rect: rect,
view: view,
configuration: configuration.viewConfiguration)
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
case .skeletonsHolderView(_):
updateGeometry(rect: rect,
view: view,
configuration: configuration.holderViewConfiguration)
}
animationLayer.frame = bounds
}
private func updateGeometry(rect: CGRect,
view: UIView,
configuration: some BaseViewSkeletonsConfiguration) {
path = configuration.createPath(for: view.bounds)
let viewFrame = CGRect(origin: rect.origin, size: path?.boundingBox.size ?? rect.size)
frame = configuration.applyPadding(viewFrame: viewFrame)
animatable = configuration.animatable
}
}

View File

@ -39,19 +39,17 @@ public extension UIView {
let viewsToSkeletons = viewsToSkeletons ?? skeletonableViews
isUserInteractionEnabled = false
configureBaseLayer(withConfiguration: config)
let baseLayer = configureBaseLayer(withConfiguration: config)
viewsToSkeletons
.flatMap { view in
getSkeletonLayer(forView: view, withConfiguration: config)
}
.map { layer in
layer.startAnimation()
return layer
}
.insert(onto: self)
.skeletonsShown()
.forEach { $0.startAnimation() }
baseLayer.startAnimation()
}
func hideSkeletons() {
@ -80,7 +78,7 @@ public extension UIView {
let config = (view as? SkeletonsPresenter)?.skeletonsConfiguration ?? conf
if view.isSkeletonsContainer {
if !conf.isContainersHidden, !forceNoContainers {
if !conf.containerViewConfiguration.isContainerHidden, !forceNoContainers {
let skeletonLayer = config.createSkeletonLayer(for: self)
skeletonLayer.bind(to: .parentView(view))
@ -102,11 +100,14 @@ public extension UIView {
}
}
private func configureBaseLayer(withConfiguration conf: SkeletonsConfiguration) {
@discardableResult
private func configureBaseLayer(withConfiguration conf: SkeletonsConfiguration) -> SkeletonLayer {
let skeletonLayer = conf.createSkeletonLayer(for: self)
skeletonLayer.bind(to: .skeletonsHolderView(self))
layer.insertSublayer(skeletonLayer, at: .max)
return skeletonLayer
}
}

View File

@ -195,8 +195,8 @@ let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in
*/
let confWithRedBackgroundColor = SkeletonsConfiguration(skeletonsBackgroundColor: .red)
//: Для скрытия view от пользователя скелетонами накладывается специальный CALayer, чей цвет заполнения равен backgroundColor view, с которой были запущены скелетоны. Однако этот цвет можно переопределить переопределить с помощью параметра `baseSkeletonBackgroundColor`
let confWithRedBaseBackgroundColor = SkeletonsConfiguration(baseSkeletonBackgroundColor: .red)
//: Для скрытия view от пользователя скелетонами накладывается специальный CALayer, чей цвет заполнения равен backgroundColor view, с которой были запущены скелетоны. Однако этот цвет можно переопределить переопределить с помощью параметра `HolderViewSkeletonsConfiguration.backgroundColor`
let confWithRedBaseBackgroundColor = SkeletonsConfiguration(holderViewConfiguration: .init(backgroundColor: .red))
/*:
### Форма

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIElements'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Bunch of useful protocols and views.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIKitCore'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIWebView'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Universal web view API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIYandexMapUtils'
s.version = '1.55.1'
s.version = '1.56.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -204,10 +204,10 @@ let confWithTopToBottomAnim = SkeletonsConfiguration(animation: { _ in
let confWithRedBackgroundColor = SkeletonsConfiguration(skeletonsBackgroundColor: .red)
```
Для скрытия view от пользователя скелетонами накладывается специальный CALayer, чей цвет заполнения равен backgroundColor view, с которой были запущены скелетоны. Однако этот цвет можно переопределить переопределить с помощью параметра `baseSkeletonBackgroundColor`
Для скрытия view от пользователя скелетонами накладывается специальный CALayer, чей цвет заполнения равен backgroundColor view, с которой были запущены скелетоны. Однако этот цвет можно переопределить переопределить с помощью параметра `HolderViewSkeletonsConfiguration.backgroundColor`
```swift
let confWithRedBaseBackgroundColor = SkeletonsConfiguration(baseSkeletonBackgroundColor: .red)
let confWithRedBaseBackgroundColor = SkeletonsConfiguration(holderViewConfiguration: .init(backgroundColor: .red))
```
### Форма