// // 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.. 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 } } }