feat: skeletons api
This commit is contained in:
parent
e9b32ce326
commit
9f5d7387d7
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UIFont?, CGFloat>?
|
||||
public var lineSpacing: Closure<UIFont?, CGFloat>?
|
||||
|
||||
public init(numberOfLines: LinesAmount = .constant(3),
|
||||
lineHeight: Closure<UIFont?, CGFloat>? = nil,
|
||||
lineSpacing: Closure<UIFont?, CGFloat>? = 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..<numberOfLines {
|
||||
let y = (lineHeight + spacing) * CGFloat(lineNumber)
|
||||
|
||||
path.move(to: CGPoint(x: cornerRadius, y: y))
|
||||
|
||||
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: y))
|
||||
path.addQuadCurve(to: CGPoint(x: rect.width, y: y + cornerRadius),
|
||||
controlPoint: CGPoint(x: rect.width, y: y))
|
||||
|
||||
path.addLine(to: CGPoint(x: rect.width, y: y + lineHeight - cornerRadius))
|
||||
path.addQuadCurve(to: CGPoint(x: rect.width - cornerRadius, y: y + lineHeight),
|
||||
controlPoint: CGPoint(x: rect.width, y: y + lineHeight))
|
||||
|
||||
path.addLine(to: CGPoint(x: cornerRadius, y: y + lineHeight))
|
||||
path.addQuadCurve(to: CGPoint(x: .zero, y: y + lineHeight - cornerRadius),
|
||||
controlPoint: CGPoint(x: .zero, y: y + lineHeight))
|
||||
|
||||
path.addLine(to: CGPoint(x: .zero, y: y + cornerRadius))
|
||||
path.addQuadCurve(to: CGPoint(x: cornerRadius, y: y),
|
||||
controlPoint: CGPoint(x: .zero, y: y))
|
||||
}
|
||||
|
||||
return path.cgPath
|
||||
}
|
||||
|
||||
open func configureLabelPath(label: UILabel) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CAAnimationGroup>?
|
||||
|
||||
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<CAAnimationGroup>? = 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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
Loading…
Reference in New Issue