How to create effect of stack of view for collection view? - swift

I am try create effect like few view stack over other for collection view, I am not build effect like this in the past and can't understand how I can create this effect programmatically without images?
Can somebody explain me how I can repeat this effect or post link to code of tutorial?
Example of this effect below:

To get that effect you'd need to add layers under the collection view cells' layers that are smaller and shifted down a little from the cell's layers. You'd use the same background color as the cell on each layer that had a lower alpha than the cell's layer.
I created a sample project that demonstrates how to get the effect:
https://github.com/DuncanMC/CustomCollectionViewCell.git
The cells look like this:
The heavy lifting is in a custom subclass of UICollectionViewCell that I called MyCollectionViewCell
Here is that class:
class MyCollectionViewCell: UICollectionViewCell {
public var contentCornerRadius: CGFloat = 0 {
didSet {
contentView.layer.cornerRadius = contentCornerRadius
sizeLayerFrames()
}
}
public var fraction: CGFloat = 0.075 { // What percent to shrink & shift the faded layers down (0.075 = 7.5%)
didSet {
sizeLayerFrames()
}
}
private var layerMask = CAShapeLayer()
private var layer1 = CALayer()
private var layer2 = CALayer()
// Use this function to set the cell's background color.
// (You can't set the view's background color, since we Don't clip the view to it's bounds.)
// Be sure to set the background color explicitly, since by default it sets a random color that will persist
// as the cells are recylced, causing your cell colors to move around as the user scrolls
public func setBackgroundColor(_ color: UIColor) {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: nil)
contentView.layer.backgroundColor = color.cgColor
//Make the first extra layer have the same color as the cell's layer, but with alpha 0.25
layer1.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 0.25).cgColor
//Make the second extra layer have the same color as the cell's layer, but with alpha 0.125
layer2.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 0.125).cgColor
}
#IBOutlet weak var customLabel: UILabel!
//Do The initial setup once the cell is loaded.
//Note that t
override func awakeFromNib() {
contentView.layer.masksToBounds = false
// Color each cell's layer some random hue (change to set whatever color you desire.)
//For testing, use a color based on a random hue and fairly high random brightness.
let hue = CGFloat.random(in: 0...360)
let brightness = CGFloat.random(in: 0.8...1.0)
layer.masksToBounds = false
setBackgroundColor(UIColor(hue: hue, saturation: 1, brightness: brightness, alpha: 1))
// Make the inside of the shape layer white (opaque), The color doesn't matter - just that the alpha value is 1
// and the outside clear (transparent)
layerMask.fillColor = UIColor.white.cgColor
layerMask.backgroundColor = UIColor.clear.cgColor
//With the even/odd rule, the inner shape will not be filled (we'll only fill the part NOT in the inner shape)
layerMask.fillRule = .evenOdd
contentCornerRadius = 30
sizeLayerFrames()
contentView.layer.addSublayer(layer1)
layer1.addSublayer(layer2)
layer1.mask = layerMask
}
private func sizeLayerFrames() {
layer1.cornerRadius = contentCornerRadius
layer2.cornerRadius = contentCornerRadius
let viewBounds = bounds //Use the layer's bounds as the starting point for the extra layers.
var frame1 = viewBounds
frame1.origin.y += viewBounds.size.height * fraction
frame1.origin.x += viewBounds.size.width * fraction
frame1.size.width *= CGFloat(1 - 2 * fraction)
layer1.frame = frame1
var frame2 = viewBounds
frame2.origin.y += viewBounds.size.height * 0.75 * fraction
frame2.origin.x += viewBounds.size.width * fraction
frame2.size.width *= CGFloat(1 - 4 * fraction)
layer2.frame = frame2
//Create a mask layer to clip the extra layers.
var maskFrame = viewBounds
//We are going to install the mask on layer1, so offeset the frame to cover the whole view contents
maskFrame.origin.y -= viewBounds.size.height * fraction
maskFrame.origin.x -= viewBounds.size.width * fraction
maskFrame.size.height += viewBounds.size.height * fraction * 1.75
layerMask.frame = maskFrame
maskFrame = viewBounds
let innerPath = UIBezierPath(roundedRect: maskFrame, cornerRadius: 30)
maskFrame.size.height += viewBounds.size.height * fraction * 1.75
let combinedPath = UIBezierPath(rect: maskFrame)
combinedPath.append(innerPath)
layerMask.path = combinedPath.cgPath
}
override var bounds: CGRect {
didSet {
sizeLayerFrames()
}
}
}

Related

How to find current color in UIImageView and change repeatedly?

I have this UIImageView where I am only changing the white color of the image. When the white color changes it doesn't change again because the white color is no longer white anymore. I want to access the new color and change it to a different color every time I press a button. Im using this func I found on github.
var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)
#IBAction func changeColors(_ sender: Any) {
let randomRGB = CGFloat.random(in: 0.0...1.0)
let randomRGB2 = CGFloat.random(in: 0.0...1.0)
let randomRGB3 = CGFloat.random(in: 0.0...1.0)
//randomly change color
var newColor = UIColor.init(red: randomRGB3, green: randomRGB2, blue: randomRGB, alpha: 1)
let changeColor = replaceColor(color: currentColor, withColor: newColor, image: mainImage.image!, tolerance: 0.5)
mainImage.image = changeColor
//change current color to new color
currentColor = newColor
}
extension ViewController {
func replaceColor(color: UIColor, withColor: UIColor, image: UIImage, tolerance: CGFloat) -> UIImage {
// This function expects to get source color(color which is supposed to be replaced)
// and target color in RGBA color space, hence we expect to get 4 color components: r, g, b, a
assert(color.cgColor.numberOfComponents == 4 && withColor.cgColor.numberOfComponents == 4,
"Must be RGBA colorspace")
// Allocate bitmap in memory with the same width and size as source image
let imageRef = image.cgImage!
let width = imageRef.width
let height = imageRef.height
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width;
let bitsPerComponent = 8
let bitmapByteCount = bytesPerRow * height
let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCount)
let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: CGColorSpace(name: CGColorSpace.genericRGBLinear)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
let rc = CGRect(x: 0, y: 0, width: width, height: height)
// Draw source image on created context
context!.draw(imageRef, in: rc)
// Get color components from replacement color
let withColorComponents = withColor.cgColor.components
let r2 = UInt8(withColorComponents![0] * 255)
let g2 = UInt8(withColorComponents![1] * 255)
let b2 = UInt8(withColorComponents![2] * 255)
let a2 = UInt8(withColorComponents![3] * 255)
// Prepare to iterate over image pixels
var byteIndex = 0
while byteIndex < bitmapByteCount {
// Get color of current pixel
let red = CGFloat(rawData[byteIndex + 0]) / 255
let green = CGFloat(rawData[byteIndex + 1]) / 255
let blue = CGFloat(rawData[byteIndex + 2]) / 255
let alpha = CGFloat(rawData[byteIndex + 3]) / 255
let currentColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
// Compare two colors using given tolerance value
if compareColor(color: color, withColor: currentColor , withTolerance: tolerance) {
// If the're 'similar', then replace pixel color with given target color
rawData[byteIndex + 0] = r2
rawData[byteIndex + 1] = g2
rawData[byteIndex + 2] = b2
rawData[byteIndex + 3] = a2
}
byteIndex = byteIndex + 4;
}
// Retrieve image from memory context
let imgref = context!.makeImage()
let result = UIImage(cgImage: imgref!)
// Clean up a bit
rawData.deallocate()
return result
}
func compareColor(color: UIColor, withColor: UIColor, withTolerance: CGFloat) -> Bool
{
var r1: CGFloat = 0.0, g1: CGFloat = 0.0, b1: CGFloat = 0.0, a1: CGFloat = 0.0;
var r2: CGFloat = 0.0, g2: CGFloat = 0.0, b2: CGFloat = 0.0, a2: CGFloat = 0.0;
color.getRed(&r1, green: &g1, blue: &b1, alpha: &a1);
withColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2);
return abs(r1 - r2) <= withTolerance &&
abs(g1 - g2) <= withTolerance &&
abs(b1 - b2) <= withTolerance &&
abs(a1 - a2) <= withTolerance;
}
}
Here are a few observations that I made which might be impacting the results you see:
As we discussed in the comments, if you want to start from the color which was changed previously, you need to hold on the color after the image has been updated beyond the scope of your function (you did this)
The next issue about ending up with one color probably has a lot to do with the fault tolerance
When you try to change a color in an image with 0.5 (50%) fault tolerance of a given color, you are changing a huge number of colors in an image in the first pass
If there were 100 colors in a color system, you are going to look for 50 of those colors in the image and change them to 1 specific color
In the first pass, you start with white. Lets say that 75% of the image has colors that are similar to white with a 50% fault tolerance - 75% of the image is going to change to that color
With such a high fault tolerance, soon enough one color will appear that will be close to most of the colors in the image with a 50% fault tolerance and you will end up with colors with 1 image
Some ideas to improve the results
Set a lower fault tolerance - you will see smaller changes and the same result could occur with 1 color but it will happen over a longer period of time
If you really want to randomize and no get this 1 color results, I suggest to change how you use the currentColor and make changes to the original image, not the updated image (I have this example below)
This will not impact the solution but better to handle optionals more safely as I see a lot of ! so I would recommend changing that
Perform the image processing in a background thread (also in the example below)
Here is an update with an example
class ImageColorVC: UIViewController
{
// UI Related
private var loaderController: UIAlertController!
let mainImage = UIImageView(image: UIImage(named: "art"))
// Save the current color and the original image
var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)
var originalImage: UIImage!
override func viewDidLoad()
{
super.viewDidLoad()
// UI configuration, you can ignore
view.backgroundColor = .white
title = "Image Color"
configureBarButton()
configureImageView()
// Store the original image
originalImage = mainImage.image!
}
// MARK: AUTO LAYOUT
private func configureBarButton()
{
let barButton = UIBarButtonItem(barButtonSystemItem: .refresh,
target: self,
action: #selector(changeColors))
navigationItem.rightBarButtonItem = barButton
}
private func configureImageView()
{
mainImage.translatesAutoresizingMaskIntoConstraints = false
mainImage.contentMode = .scaleAspectFit
mainImage.clipsToBounds = true
view.addSubview(mainImage)
view.addConstraints([
mainImage.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
mainImage.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
mainImage.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
mainImage.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
// Configures a loader to show while image is processing
private func configureLoaderController()
{
loaderController = UIAlertController(title: nil,
message: "Processing",
preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10,
y: 5,
width: 50,
height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.style = UIActivityIndicatorView.Style.medium
loadingIndicator.startAnimating();
loaderController.view.addSubview(loadingIndicator)
}
//MARK: FACTORY
// Similar to your function, only difference is that it uses
// the original image
private func performChangeColors()
{
let randomRGB = CGFloat.random(in: 0.0...1.0)
let randomRGB2 = CGFloat.random(in: 0.0...1.0)
let randomRGB3 = CGFloat.random(in: 0.0...1.0)
//randomly change color
let newColor = UIColor.init(red: randomRGB3,
green: randomRGB2,
blue: randomRGB,
alpha: 1)
// Do work in the back ground
DispatchQueue.global(qos: .background).async
{
let imageWithNewColor = self.replaceColor(color: self.currentColor,
withColor: newColor,
image: self.originalImage!,
tolerance: 0.5)
// Update the UI on the main thread
DispatchQueue.main.async
{
self.updateImageView(with: imageWithNewColor)
//change current color to new color
self.currentColor = newColor
}
}
}
#objc
private func changeColors()
{
// Configure a loader to show while image is processing
configureLoaderController()
present(loaderController, animated: true) { [weak self] in
self?.performChangeColors()
}
}
// Update the UI
private func updateImageView(with image: UIImage)
{
dismiss(animated: true) { [weak self] in
self?.mainImage.image = image
}
}
}
After starting with this:
About 50 tries later, it still seems to work well:
You can watch a longer video here to see a few more color changes that happen without leading to one single color
Hope this gives you enough to create the required workaround for your solution

Swift - Creating shadow with 2 different colours for imageView

I'm wondering how to create a shadow with two different colours for an imageView. For example the top and left side has a different colour than the right and bottom side of the imageView.
To get different color shadows - one going up-left and one going down-right - on a UIImageView, one approach would be:
Subclass UIView
Give it 3 CALayer sublayers
Shadow 1 layer
Shadow 2 layer
Image layer
This also makes it easy to add rounded corners.
Here is a sample class. It has #IBInspectable properties to set the image, corner radius, shadow colors and shadow offsets. It is also marked #IBDesignable so you can see how it looks while designing in Storyboard / Interface Builder:
#IBDesignable
class DoubleShadowRoundedImageView: UIView {
#IBInspectable var image: UIImage? = nil {
didSet {
imageLayer.contents = image?.cgImage
}
}
#IBInspectable var cornerRadius: CGFloat = 0.0
#IBInspectable var shad1X: CGFloat = 0.0
#IBInspectable var shad1Y: CGFloat = 0.0
#IBInspectable var shad2X: CGFloat = 0.0
#IBInspectable var shad2Y: CGFloat = 0.0
#IBInspectable var shad1Color: UIColor = UIColor.blue
#IBInspectable var shad2Color: UIColor = UIColor.red
var imageLayer: CALayer = CALayer()
var shadowLayer1: CALayer = CALayer()
var shadowLayer2: CALayer = CALayer()
var shape: UIBezierPath {
return UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
}
var shapeAsPath: CGPath {
return shape.cgPath
}
var shapeAsMask: CAShapeLayer {
let s = CAShapeLayer()
s.path = shapeAsPath
return s
}
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = false
backgroundColor = .clear
self.layer.addSublayer(shadowLayer1)
self.layer.addSublayer(shadowLayer2)
self.layer.addSublayer(imageLayer)
imageLayer.frame = bounds
imageLayer.mask = shapeAsMask
shadowLayer1.frame = bounds
shadowLayer2.frame = bounds
shadowLayer1.shadowPath = (image == nil) ? nil : shapeAsPath
shadowLayer1.shadowOpacity = 0.80
shadowLayer2.shadowPath = (image == nil) ? nil : shapeAsPath
shadowLayer2.shadowOpacity = 0.80
shadowLayer1.shadowColor = shad1Color.cgColor
shadowLayer2.shadowColor = shad2Color.cgColor
shadowLayer1.shadowOffset = CGSize(width: shad1X, height: shad1Y)
shadowLayer2.shadowOffset = CGSize(width: shad2X, height: shad2Y)
}
}
You would probably want to change some of the default values, and you might want to add some additional properties (such as shadow opacity).
Example results:

Add glow effect to text of a UI button - swift

my question is quite straight forward. I am aiming to add a basic glow effect to a button in swift. I want the text to glow, not the entire button box.
I have attached an image as an example to illustrate what I am aiming to achieve.
I have looked elsewhere but typically only find animations which is not what I want. Sorry the image is of poor quality.
I am currently using this code but my settings button appears with a very weak glow, how can I make it stronger:
import UIKit
extension UIView {
enum GlowEffect: Float {
case small = 0.4, normal = 2, big = 30
}
func doGlowAnimation(withColor color: UIColor, withEffect effect: GlowEffect = .normal) {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowRadius = 0
layer.shadowOpacity = 1
layer.shadowOffset = .zero
let glowAnimation = CABasicAnimation(keyPath: "shadowRadius")
glowAnimation.fromValue = 20 // effect.rawValue
glowAnimation.toValue = 20
glowAnimation.fillMode = .removed
glowAnimation.repeatCount = .infinity
layer.add(glowAnimation, forKey: "shadowGlowingAnimation")
}
}
Changing the intensity doesn't give that strong color effect near the individual letters
To create an outer glow effect on the title of a UIButton you'll want to make sure you adjust the shadow properties of the UIButton's titleLabel. Meaning you could run your animation by saying:
button.titleLabel?.doGlowAnimation(withColor: UIColor.yellow)
The animation adjusts shadowRadius though currently goes from 20 to 20 so there's no actual animation.
extension UIView {
enum GlowEffect: Float {
case small = 0.4, normal = 2, big = 30
}
func doGlowAnimation(withColor color: UIColor, withEffect effect: GlowEffect = .normal) {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowRadius = 0
layer.shadowOpacity = 0.8
layer.shadowOffset = .zero
let glowAnimation = CABasicAnimation(keyPath: "shadowRadius")
glowAnimation.fromValue = 0
glowAnimation.toValue = effect.rawValue
glowAnimation.fillMode = .removed
glowAnimation.repeatCount = .infinity
glowAnimation.duration = 2
glowAnimation.autoreverses = true
layer.add(glowAnimation, forKey: "shadowGlowingAnimation")
}
}
Provides a pulsating outer glow, growing over the course of two seconds then reversing.

Get the color needed to be composed with another one to get a target color in swift

I use an NSTableView to draw some information in NSTableCellView(s). I want to set the background color of the NSTableView to a certain value and the NSTableCellView's background color to another value independently of the alpha component of the colors used. The problem is that if I set a background color with alpha component 0.3 to NSTableCellView, we see the background color of the NSTableView and then the color is not what I set.
I see two options to solve this problem:
draw the background color of the NSTableView without drawing under the rects used by the NSTableCellView(s).
use color theory and CoreGraphics to compute the new color.
I have worked around a bit option 1 and haven't got any result. I am now looking more into option 2.
For example, if I have two colors:
let tableViewBackgroundColor = NSColor(calibratedRed: 48/255, green: 47/255, blue: 46/255, alpha: 1)
let tableViewCellBackgroundColor = NSColor(calibratedRed: 42/255, green: 41/255, blue: 40/255, alpha: 1)
I want, that the resulting color applied to NSTableCellView background:
let targetColor = tableViewCellBackgroundColor.withAplphaComponent(0.3)
even when the color:
let tableViewBackgroundColorWithAlpha = tableViewBackgroundColor.withAlphaComponent(0.3)
is applied to the background of the NSTableView.
I am looking for an extension to NSColor (CGColor would work) like this:
extension NSColor {
///
/// Return the color that needs to be composed with the color parameter
/// in order to result in the current (self) color.
///
func composedColor(with color: NSColor) -> NSColor
}
That could be used like this:
let color = targetColor.composedColor(with:
tableViewBackgroundColorWithAlpha)
Any idea?
Answering my own question, the solution for this problem has finally been to avoid drawing the parts with table view cells (option 1 on the question). I inspired myself from Drawing Custom Alternating Row Backgrounds in NSTableViews with Swift to implement the final solution.
It basically involve overriding the method func drawBackground(inClipRect clipRect: NSRect) of the NSTableView but avoiding drawing the background part of the table where there is cells present.
Here is the solution:
override func drawBackground(inClipRect clipRect: NSRect) {
super.drawBackground(inClipRect: clipRect)
drawTopBackground(inClipRect: clipRect)
drawBottomBackground(inClipRect: clipRect)
}
private func drawTopBackground(inClipRect clipRect: NSRect) {
guard clipRect.origin.y < 0 else { return }
let rectHeight = rowHeight + intercellSpacing.height
let minY = NSMinY(clipRect)
var row = 0
currentBackgroundColor.setFill()
while true {
let rowRect = NSRect(x: 0, y: (rectHeight * CGFloat(row) - rectHeight), width: NSMaxX(clipRect), height: rectHeight)
if self.rows(in: rowRect).isEmpty {
rowRect.fill()
}
if rowRect.origin.y < minY { break }
row -= 1
}
}
private func drawBottomBackground(inClipRect clipRect: NSRect) {
let rectHeight = rowHeight + intercellSpacing.height
let maxY = NSMaxY(clipRect)
var row = rows(in: clipRect).location
currentBackgroundColor.setFill()
while true {
let rowRect = NSRect(
x: 0,
y: (rectHeight * CGFloat(row)),
width: NSMaxX(clipRect),
height: rectHeight)
if self.rows(in: rowRect).isEmpty {
rowRect.fill()
}
if rowRect.origin.y > maxY { break }
row += 1
}
}

Color animation

I've written simple animations for drawing rectangles in lines, we can treat them as a bars.
Each bar is one shape layer which has a path which animates ( size change and fill color change ).
#IBDesignable final class BarView: UIView {
lazy var pathAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
let red = UIColor(red: 249/255, green: 26/255, blue: 26/255, alpha: 1)
let orange = UIColor(red: 1, green: 167/255, blue: 463/255, alpha: 1)
let green = UIColor(red: 106/255, green: 239/255, blue: 47/255, alpha: 1)
lazy var backgroundColorAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "fillColor")
animation.duration = 1
animation.fromValue = red.cgColor
animation.byValue = orange.cgColor
animation.toValue = green.cgColor
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
#IBInspectable var spaceBetweenBars: CGFloat = 10
var numberOfBars: Int = 5
let data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
override func awakeFromNib() {
super.awakeFromNib()
initSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayers()
}
func setupLayers() {
let width = bounds.width - (spaceBetweenBars * CGFloat(numberOfBars + 1)) // There is n + 1 spaces between bars.
let barWidth: CGFloat = width / CGFloat(numberOfBars)
let scalePoint: CGFloat = bounds.height / 10.0 // 10.0 - 10 points is max
guard let sublayers = layer.sublayers as? [CAShapeLayer] else { return }
for i in 0...numberOfBars - 1 {
let barHeight: CGFloat = scalePoint * data[i]
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
var xPos: CGFloat!
if i == 0 {
xPos = spaceBetweenBars
} else if i == numberOfBars - 1 {
xPos = bounds.width - (barWidth + spaceBetweenBars)
} else {
xPos = barWidth * CGFloat(i) + spaceBetweenBars * CGFloat(i) + spaceBetweenBars
}
let startPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: 0)).cgPath
let endPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: -barHeight)).cgPath
sublayers[i].path = startPath
pathAnimation.toValue = endPath
sublayers[i].removeAllAnimations()
sublayers[i].add(pathAnimation, forKey: "path")
sublayers[i].add(backgroundColorAnimation, forKey: "backgroundColor")
}
}
func initSublayers() {
for _ in 1...numberOfBars {
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
}
}
}
The size ( height ) of bar depends of the data array, each sublayers has a different height. Based on this data I've crated a scale.
PathAnimation is changing height of the bars.
BackgroundColorAnimation is changing the collors of the path. It starts from red one, goes through the orange and finish at green.
My goal is to connect backgroundColorAnimation with data array as well as it's connected with pathAnimation.
Ex. When in data array is going to be value 1.0 then the bar going to be animate only to the red color which is a derivated from a base red color which is declared as a global variable. If the value in the data array going to be ex. 4.5 then the color animation will stop close to the delcared orange color, the 5.0 limit going to be this orange color or color close to this. Value closer to 10 going to be green.
How could I connect these conditions with animation properties fromValue, byValue, toValue. Is it an algorithm for that ? Any ideas ?
You have several problems.
You're setting fillMode and isRemovedOnCompletion. This tells me, to be blunt, that you don't understand Core Animation. You need to watch WWDC 2011 Session 421: Core Animation Essentials.
You're adding more layers every time layoutSubviews is called, but not doing anything with them.
You're adding animation every time layoutSubviews runs. Do you really want to re-animate the bars when the double-height “in-call” status bar appears or disappears, or on an interface rotation? It's probably better to have a separate animateBars() method, and call it from your view controller's viewDidAppear method.
You seem to think byValue means “go through this value on the way from fromValue to toValue”, but that's not what it means. byValue is ignored in your case, because you're setting fromValue and toValue. The effects of byValue are explained in Setting Interpolation Values.
If you want to interpolate between colors, it's best to use a hue-based color space, but I believe Core Animation uses an RGB color space. So you should use a keyframe animation to specify intermediate colors that you calculate by interpolating in a hue-based color space.
Here's a rewrite of BarView that fixes all these problems:
#IBDesignable final class BarView: UIView {
#IBInspectable var spaceBetweenBars: CGFloat = 10
var data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
var maxDatum = CGFloat(10)
func animateBars() {
guard window != nil else { return }
let bounds = self.bounds
var flatteningTransform = CGAffineTransform.identity.translatedBy(x: 0, y: bounds.size.height).scaledBy(x: 1, y: 0.001)
let duration: CFTimeInterval = 1
let frames = Int((duration * 60.0).rounded(.awayFromZero))
for (datum, barLayer) in zip(data, barLayers) {
let t = datum / maxDatum
if let path = barLayer.path {
let path0 = path.copy(using: &flatteningTransform)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = 1
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
pathAnimation.fromValue = path0
barLayer.add(pathAnimation, forKey: pathAnimation.keyPath)
let colors = gradient.colors(from: 0, to: t, count: frames).map({ $0.cgColor })
let colorAnimation = CAKeyframeAnimation(keyPath: "fillColor")
colorAnimation.timingFunction = pathAnimation.timingFunction
colorAnimation.duration = duration
colorAnimation.values = colors
barLayer.add(colorAnimation, forKey: colorAnimation.keyPath)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
createOrDestroyBarLayers()
let bounds = self.bounds
let barSpacing = (bounds.size.width - spaceBetweenBars) / CGFloat(data.count)
let barWidth = barSpacing - spaceBetweenBars
for ((offset: i, element: datum), barLayer) in zip(data.enumerated(), barLayers) {
let t = datum / maxDatum
let barHeight = t * bounds.size.height
barLayer.frame = bounds
let rect = CGRect(x: spaceBetweenBars + CGFloat(i) * barSpacing, y: bounds.size.height, width: barWidth, height: -barHeight)
barLayer.path = CGPath(rect: rect, transform: nil)
barLayer.fillColor = gradient.color(at: t).cgColor
}
}
private let gradient = Gradient(startColor: .red, endColor: .green)
private var barLayers = [CAShapeLayer]()
private func createOrDestroyBarLayers() {
while barLayers.count < data.count {
barLayers.append(CAShapeLayer())
layer.addSublayer(barLayers.last!)
}
while barLayers.count > data.count {
barLayers.removeLast().removeFromSuperlayer()
}
}
}
private extension UIColor {
var hsba: [CGFloat] {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return [hue, saturation, brightness, alpha]
}
}
private struct Gradient {
init(startColor: UIColor, endColor: UIColor) {
self.startColor = startColor
self.startHsba = startColor.hsba
self.endColor = endColor
self.endHsba = endColor.hsba
}
let startColor: UIColor
let endColor: UIColor
let startHsba: [CGFloat]
let endHsba: [CGFloat]
func color(at t: CGFloat) -> UIColor {
let out = zip(startHsba, endHsba).map { $0 * (1.0 - t) + $1 * t }
return UIColor(hue: out[0], saturation: out[1], brightness: out[2], alpha: out[3])
}
func colors(from t0: CGFloat, to t1: CGFloat, count: Int) -> [UIColor] {
var colors = [UIColor]()
colors.reserveCapacity(count)
for i in 0 ..< count {
let s = CGFloat(i) / CGFloat(count - 1)
let t = t0 * (1 - s) + t1 * s
colors.append(color(at: t))
}
return colors
}
}
Result: