Files

230 lines
8.9 KiB
Swift

//
// Copyright SkeletonView. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// CALayer+Tint.swift
//
// Created by Juanpe Catalán on 18/8/21.
import UIKit
extension CAGradientLayer {
override func tint(withColors colors: [UIColor], traitCollection: UITraitCollection?) {
skeletonSublayers.recursiveSearch(leafBlock: {
if #available(iOS 13.0, tvOS 13, *), let traitCollection = traitCollection {
self.colors = colors.map { $0.resolvedColor(with: traitCollection).cgColor }
} else {
self.colors = colors.map { $0.cgColor }
}
}) {
$0.tint(withColors: colors, traitCollection: traitCollection)
}
}
}
extension CALayer {
enum Constants {
static let skeletonSubLayersName = "SkeletonSubLayersName"
}
var skeletonSublayers: [CALayer] {
return sublayers?.filter { $0.name == Constants.skeletonSubLayersName } ?? [CALayer]()
}
@objc func tint(withColors colors: [UIColor], traitCollection: UITraitCollection?) {
skeletonSublayers.recursiveSearch(leafBlock: {
if #available(iOS 13.0, tvOS 13, *), let traitCollection = traitCollection {
backgroundColor = colors.first?.resolvedColor(with: traitCollection).cgColor
} else {
backgroundColor = colors.first?.cgColor
}
}) {
$0.tint(withColors: colors, traitCollection: traitCollection)
}
}
func playAnimation(_ anim: SkeletonLayerAnimation, key: String, completion: (() -> Void)? = nil) {
skeletonSublayers.recursiveSearch(leafBlock: {
DispatchQueue.main.async { CATransaction.begin() }
DispatchQueue.main.async { CATransaction.setCompletionBlock(completion) }
add(anim(self), forKey: key)
DispatchQueue.main.async { CATransaction.commit() }
}) {
$0.playAnimation(anim, key: key, completion: completion)
}
}
func stopAnimation(forKey key: String) {
skeletonSublayers.recursiveSearch(leafBlock: {
removeAnimation(forKey: key)
}) {
$0.stopAnimation(forKey: key)
}
}
func setOpacity(from: Int, to: Int, duration: TimeInterval, completion: (() -> Void)?) {
DispatchQueue.main.async { CATransaction.begin() }
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
animation.fromValue = from
animation.toValue = to
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
DispatchQueue.main.async { CATransaction.setCompletionBlock(completion) }
add(animation, forKey: "setOpacityAnimation")
DispatchQueue.main.async { CATransaction.commit() }
}
func insertSkeletonLayer(_ sublayer: SkeletonLayer, atIndex index: UInt32, transition: SkeletonTransitionStyle, completion: (() -> Void)? = nil) {
insertSublayer(sublayer.contentLayer, at: index)
switch transition {
case .none:
DispatchQueue.main.async { completion?() }
case .crossDissolve(let duration):
sublayer.contentLayer.setOpacity(from: 0, to: 1, duration: duration, completion: completion)
}
}
}
private extension CALayer {
func alignLayerFrame(_ rect: CGRect, paddingInsets: UIEdgeInsets, alignment: NSTextAlignment, isRTL: Bool) -> CGRect {
var newRect = rect
let superlayerWidth = (superlayer?.bounds.width ?? 0)
switch alignment {
case .natural where isRTL,
.right:
newRect.origin.x = superlayerWidth - rect.width - paddingInsets.right
case .center:
newRect.origin.x = (superlayerWidth + paddingInsets.left - paddingInsets.right - rect.width) / 2
case .natural, .left, .justified:
break
@unknown default:
break
}
return newRect
}
func calculatedWidthForLine(at index: Int, totalLines: Int, lastLineFillPercent: Int, paddingInsets: UIEdgeInsets) -> CGFloat {
var width = bounds.width - paddingInsets.left - paddingInsets.right
if index == totalLines - 1 {
width = width * CGFloat(lastLineFillPercent) / 100
}
return width
}
func calculateNumLines(for config: SkeletonMultilinesLayerConfig) -> Int {
let definedNumberOfLines = config.lines
let requiredSpaceForEachLine = config.lineHeight + config.multilineSpacing
let neededLines = round(CGFloat(bounds.height - config.paddingInsets.top - config.paddingInsets.bottom) / CGFloat(requiredSpaceForEachLine))
guard neededLines.isNormal else {
return 0
}
let calculatedNumberOfLines = Int(neededLines)
guard calculatedNumberOfLines > 0 else {
return 1
}
if definedNumberOfLines > 0, definedNumberOfLines <= calculatedNumberOfLines {
return definedNumberOfLines
}
return calculatedNumberOfLines
}
}
extension CALayer {
func addMultilinesLayers(for config: SkeletonMultilinesLayerConfig) {
let numberOfSublayers = config.lines > 0 ? config.lines : calculateNumLines(for: config)
var height = config.lineHeight
if numberOfSublayers == 1 && SkeletonAppearance.default.renderSingleLineAsView {
height = bounds.height
}
let layerBuilder = SkeletonMultilineLayerBuilder()
.setSkeletonType(config.type)
.setCornerRadius(config.multilineCornerRadius)
.setMultilineSpacing(config.multilineSpacing)
.setPadding(config.paddingInsets)
.setHeight(height)
.setAlignment(config.alignment)
.setIsRTL(config.isRTL)
(0..<numberOfSublayers).forEach { index in
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: config.lastLineFillPercent, paddingInsets: config.paddingInsets)
if let layer = layerBuilder
.setIndex(index)
.setWidth(width)
.build() {
addSublayer(layer)
}
}
}
func updateMultilinesLayers(for config: SkeletonMultilinesLayerConfig) {
let currentSkeletonSublayers = skeletonSublayers
let numberOfSublayers = currentSkeletonSublayers.count
let lastLineFillPercent = config.lastLineFillPercent
let paddingInsets = config.calculatedPaddingInsets
let multilineSpacing = config.multilineSpacing
var height = config.lineHeight
if numberOfSublayers == 1 && SkeletonAppearance.default.renderSingleLineAsView {
height = bounds.height
}
for (index, layer) in currentSkeletonSublayers.enumerated() {
let width = calculatedWidthForLine(at: index, totalLines: numberOfSublayers, lastLineFillPercent: lastLineFillPercent, paddingInsets: paddingInsets)
layer.updateLayerFrame(for: index,
totalLines: numberOfSublayers,
size: CGSize(width: width, height: height),
multilineSpacing: multilineSpacing,
paddingInsets: paddingInsets,
alignment: config.alignment,
isRTL: config.isRTL
)
}
guard config.shouldCenterVertically,
let maxY = currentSkeletonSublayers.last?.frame.maxY else {
return
}
let verticallyCenterAlignedFrames = currentSkeletonSublayers.map { layer -> CGRect in
let moveDownBy = (bounds.height - (maxY + paddingInsets.top + paddingInsets.bottom)) / 2
return layer.frame.offsetBy(dx: 0, dy: moveDownBy)
}
for (index, layer) in currentSkeletonSublayers.enumerated() {
layer.frame = verticallyCenterAlignedFrames[index]
}
}
func updateLayerFrame(for index: Int, totalLines: Int, size: CGSize, multilineSpacing: CGFloat, paddingInsets: UIEdgeInsets, alignment: NSTextAlignment, isRTL: Bool) {
let spaceRequiredForEachLine = size.height + multilineSpacing
let newFrame = CGRect(x: paddingInsets.left,
y: CGFloat(index) * spaceRequiredForEachLine + paddingInsets.top,
width: size.width,
height: size.height)
if index == totalLines - 1 {
frame = alignLayerFrame(newFrame, paddingInsets: paddingInsets, alignment: alignment, isRTL: isRTL)
} else {
frame = newFrame
}
}
}