Why is CALayer's opacity value not reflected as UIView's alpha? - iphone

I created the method below as part of custom CAAnimationGroup. The method first adds itself to weak reference to a CALayer assigned at initialization.
Then it iterates over it's own animations array and applies each animation 's toValue to the associated keyPath using KVC on the weak CALayer reference.
final public class FAAnimationGroup : CAAnimationGroup {
weak var weakLayer : CALayer?
override init() {
super.init()
animations = [CAAnimation]()
fillMode = kCAFillModeForwards
removedOnCompletion = true
}
override public func copyWithZone(zone: NSZone) -> AnyObject {
let animationGroup = super.copyWithZone(zone) as! FAAnimationGroup
animationGroup.weakLayer = weakLayer
return animationGroup
}
......
func applyFinalState() {
guard let animationLayer = weakLayer else {
return
}
animationLayer.addAnimation(self, forKey: self.animationKey)
if let groupAnimations = animations {
for animation in groupAnimations {
if let toValue = animation.toValue {
animationLayer.setValue(toValue, forKeyPath: animation.keyPath!)
}
}
}
}
}
So everything works accordingly for bounds, size, transform, and alpha for all my views just as expected with the current removedOnCompletion flag and fillMode values.
Once the animation is complete, I query the UIView, and it's backing layer. What is see is the frame reflects the correct result, the view's alpha reflects the animated opacity value. Great!
But here comes the fun part. When animation the opacity of a UISlider from 0.0 to 1.0. Once the animation is complete, I begin to adjust the UISlider value, and right as I move it, the alpha goes back to 0.0.
I tried to set the removedCompletion flag to false, and as expected, keeping the animation around kept the layer in it's final state, but that is not what I wanted. I need it to remove itself after finishing, since I did set the values directly on the the backing layer.
So after setting the removedCompletion back to true, I tried the following which has me completely stumped leading up to my question....
.....
if let groupAnimations = animations {
for animation in groupAnimations {
if let toValue = animation.toValue {
if animation.keyPath! == "opacity" {
animationLayer.owningView()!.setValue(toValue, forKeyPath: "alpha")
} else {
animationLayer.setValue(toValue, forKeyPath: animation.keyPath!)
}
}
}
}
In the code above, I would, instead of setting opacity on the layer, I set the alpha value on the owningView associated with animating layer (aka the layer's delegate). In this instance everything worked accordingly, I adjusted the slider and it did not reset to alpha 0.0
The fact that this is happening only with a UISlider is possibly irrelevant. I thought that by setting the UIView's properties, the backing layer will reflect the equivalent, and I assumed the vice versa to also be true.
Question
Why are the final alpha/opacity values in sync when I set the alpha of the view, but not reflected when I set the opacity on it's backing layer? What is the relationship between UIView and CALayer in this specific example?
From what I understood the two are very intricately interlinked, the UIView is kind of a wrapper full of access to the backing layer which redraws itself accordingly. What is this opacity/alpha relationship in the context of animations?

Related

How to detect which CAShapeLayer was clicked on in macOS using Swift?

I have a view with many CAShapeLayer objects (there can be other CALayer objects as well) and I want to modify the CAShapeLayer object that the user clicks on.
I was experimenting with the below two methods but none of them works. Any tips would be great.
Approach one:
private func modifyDrawing(at point: NSPoint) {
for layer in view.layer!.sublayers! {
let s = layer.hitTest(point)
if s != nil && s is CAShapeLayer {
selectedShape = s as? CAShapeLayer
}
}
// modify some properties
selectedShape?.shadowRadius = 20
selectedShape?.shadowOpacity = 1
selectedShape?.shadowColor = CGColor.black
}
Approach two:
private func modifyDrawing(at point: NSPoint) {
let drawingsAtMouseClick: [CAShapeLayer] = view.layer!.sublayers!.compactMap{ $0 as? CAShapeLayer }
if drawingsAtMouseClick.isEmpty {
return
}
for drawing in drawingsAtMouseClick {
if drawing.contains(point) {
selectedShape = drawing
break
}
}
// modify some properties
selectedShape?.shadowRadius = 20
selectedShape?.shadowOpacity = 1
selectedShape?.shadowColor = CGColor.black
}
The point parameter passed to these functions is the NSEvent.locationInWindow. Not sure whether I should convert this with respect to the CAShapeLayer or something.
P.S.: This isn't production code so please kindly ignore Swift best practices, etc.
The CALayer.hitTest(_:) method will tell you the layer that was hit, including in a layer's sublayers.
You shouldn't need to check every sublayer. You should be able to ask your view's layer what layer was hit by asking the top-level layer.
A view's layer's coordinates are generally the same as the view's bounds. It's anchored at 0,0 in the parent layer, and the sublayers use that coordinate space. Thus, you should convert your point to view/layer coordinates before hit testing.
(I always have to go check to see which coordinate systems are flipped from the other between iOS and Mac OS and views and layers. You might need to flip coordinates. I leave that research up to you.)
Edit:
I seem to remember that CALayer.hitTest(_:) just checks that the layer's frame contains the point, not that it actually contains an opaque pixel at that position. It's more complex if you want to check to see if the point contains an opaque pixel.

UIView replicate CAAnimation of another view, in real time?

So I've got a background view with a gradient sublayer, animating continuously to change the colors slowly. I'm doing it with a CATransaction, because I need to animate other properties as well:
CATransaction.begin()
gradientLayer.add(colorAnimation, forKey: "colors")
// other animations
CATransaction.setCompletionBlock({
// start animation again, loop forever
}
CATransaction.commit()
Now I want to replicate this gradient animation, let's say, for the title of a button for instance.
Note 1: I can't just "make a hole" in the button, if such a thing is possible, because I might have other opaque views between the button and the background.
Note 2: The gradient position on the button is not important. I don't want the text gradient to replicate the exact colors underneath, but rather to mimic the "mood" of the background.
So when the button is created, I add its gradient sublayer to a list of registered layers, that the background manager will update as well:
func register(layer: CAGradientLayer) {
let pointer = Unmanaged.passUnretained(layer).toOpaque()
registeredLayers.addPointer(pointer)
}
So while it's easy to animate the text gradient at the next iteration of the animation, I would prefer that the button starts animating as soon as it's added, since the animation usually takes a few seconds. How can I copy the background animation, i.e. set the text gradient to the current state of the background animation, and animate it with the right duration left and timing function?
The solution was indeed to use the beginTime property, as suggested by #Shivam Gaur's comment. I implemented it as follows:
// The background layer, with the original animation
var backgroundLayer: CAGradientLayer!
// The animation
var colorAnimation: CABasicAnimation!
// Variable to store animation begin time
var animationBeginTime: CFTimeInterval!
// Registered layers replicating the animation
private var registeredLayers: NSPointerArray = NSPointerArray.weakObjects()
...
// Somewhere in our code, the setup function
func setup() {
colorAnimation = CABasicAnimation(keyPath: "colors")
// do the animation setup here
...
}
...
// Called by an external class when we add a view that should replicate the background animation
func register(layer: CAGradientLayer) {
// Store a pointer to the layer in our array
let pointer = Unmanaged.passUnretained(layer).toOpaque()
registeredLayers.addPointer(pointer)
layer.colors = colorAnimation.toValue as! [Any]?
// HERE'S THE KEY: We compute time elapsed since the beginning of the animation, and start the animation at that time, using 'beginTime'
let timeElapsed = CACurrentMediaTime() - animationBeginTime
colorAnimation.beginTime = -timeElapsed
layer.add(colorAnimation, forKey: "colors")
colorAnimation.beginTime = 0
}
// The function called recursively for an endless animation
func animate() {
// Destination layer
let toLayer = newGradient() // some function to create a new color gradient
toLayer.frame = UIScreen.main.bounds
// Setup animation
colorAnimation.fromValue = backgroundLayer.colors;
colorAnimation.toValue = toLayer.colors;
// Update background layer
backgroundLayer.colors = toLayer.colors
// Update registered layers (iterate is a custom function I declared as an extension of NSPointerArray)
registeredLayers.iterate() { obj in
guard let layer = obj as? CAGradientLayer else { return }
layer.colors = toLayer.colors
}
CATransaction.begin()
CATransaction.setCompletionBlock({
animate()
})
// Add animation to background
backgroundLayer.add(colorAnimation, forKey: "colors")
// Store starting time
animationBeginTime = CACurrentMediaTime();
// Add animation to registered layers
registeredLayers.iterate() { obj in
guard let layer = obj as? CAGradientLayer else { return }
layer.add(colorAnimation, forKey: "colors")
}
CATransaction.commit()
}

UIViewPropertyAnimator AutoLayout Completion Issue

I'm using UIViewPropertyAnimator to run an array interactive animations, and one issue I'm having is that whenever the I reverse the animations I can't run the animations back forward again.
I'm using three functions to handle the animations in conjunction with a pan gesture recognizer.
private var runningAnimations = [UIViewPropertyAnimator]()
private func startInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {
if runningAnimations.isEmpty {
animateTransitionIfNeeded(gestureRecognizer: gestureRecognizer, state: state, duration: duration)
}
for animator in runningAnimations {
animator.pauseAnimation()
animationProgressWhenInterrupted = animator.fractionComplete
}
}
private func animateTransitionIfNeeded(gestureRecognizer: UIPanGestureRecognizer, state: ForegroundState, duration: TimeInterval) {
guard runningAnimations.isEmpty else {
return
}
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
switch state {
case .expanded:
// change frame
case .collapsed:
// change frame
}
}
frameAnimator.isReversed = false
frameAnimator.addCompletion { _ in
print("remove all animations")
self.runningAnimations.removeAll()
}
self.runningAnimations.append(frameAnimator)
for animator in runningAnimations {
animator.startAnimation()
}
}
private func updateInteractiveTransition(gestureRecognizer: UIPanGestureRecognizer, fractionComplete: CGFloat) {
if runningAnimations.isEmpty {
print("empty")
}
for animator in runningAnimations {
animator.fractionComplete = fractionComplete + animationProgressWhenInterrupted
}
}
What I've noticed is after I reverse the animations and then call animateTransitionIfNeeded, frameAnimator is appended to running animations however when I call updateInteractiveTransition immediately after and check runningAnimations, it's empty.
So I'm led to believe that this may have to do with how swift handles memory possibly or how UIViewAnimating completes animations.
Any suggestions?
I've come to realize the issue I was having the result of how UIViewPropertyAnimator handles layout constraints upon reversal.
I couldn't find much detail on it online or in the official documentation, but I did find this which helped a lot.
Animator just animates views into new frames. However, reversed or not, the new constraints still hold regardless of whether you reversed the animator or not. Therefore after the animator finishes, if later autolayout again lays out views, I would expect the views to go into places set by currently active constraints. Simply said: The animator animates frame changes, but not constraints themselves. That means reversing animator reverses frames, but it does not reverse constraints - as soon as autolayout does another layout cycle, they will be again applied.
Like normal you set your constraints and call view.layoutIfNeeded()
animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
[unowned self] in
switch state {
case .expanded:
self.constraintA.isActive = false
self.constraintB.isActive = true
self.view.layoutIfNeeded()
case .collapsed:
self.constraintB.isActive = false
self.constraintA.isActive = true
self.view.layoutIfNeeded()
}
}
And now, since our animator has the ability to reverse, we add a completion handler to ensure that the correct constraints are active upon completion by using the finishing position.
animator.addCompletion { [weak self] (position) in
if position == .start {
switch state {
case .collapsed:
self?.constraintA.isActive = false
self?.constraintB.isActive = true
self?.view.layoutIfNeeded()
case .expanded:
self?.constraintA.isActive = false
self?.constraintB.isActive = true
self?.view.layoutIfNeeded()
}
}
}
The animator operates on animatable properties of views, such as the frame, center, alpha, and transform properties, creating the needed animations from the blocks you provide.
This is the crucial part of the documentation.
You can properly animate:
frame, center, alpha and transform, so you would not be able to animate properly NSConstraints.
You should modify frames of views inside of addAnimations block

Swift IBInspectable didSet versus get/set

I am relatively new to IBDesignables and IBInspectable's and I noticed that a lot of tutorial use IBInspectable in this fashion.
#IBInspectable var buttonBorderWidth: CGFloat = 1.0 {
didSet {
updateView()
}
}
func updateView() {
// Usually there are more entries here for each IBInspectable
self.layer.borderWidth = buttonBorderWidth
}
But in some instances they use get and set like this for example
#IBInspectable
var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
Can someone please explain: What is happening in each of these cases and how to choose which one to use?
I see two questions. The first one is “What is happening in each of these cases”, and is best answered by reading the “Properties” chapter of The Swift Programming Language. There are also already three other answers posted which address the first question, but none of them answer the second, and more interesting, question.
The second question is “how to choose which one to use”.
Your shadowOpacity example (which is a computed property) has the following advantages over your buttonBorderWidth example (which is a stored property with an observer):
All of the shadowOpacity-related code is in one place, so it's easier to understand how it works. The buttonBorderWidth code is spread between didSet and updateViews. In a real program these functions are more likely to be farther apart, and as you said, “Usually there are more entries here for each IBInspectable”. This makes it harder to find and understand all the code involved in implementing buttonBorderWidth.
Since the view's shadowOpacity property getter and setter just forward to the layer's property, the view's property doesn't take any additional space in the view's memory layout. The view's buttonBorderWidth, being a stored property, does take additional space in the view's memory layout.
There is an advantage to the separate updateViews here, but it is subtle. Notice that buttonBorderWidth has a default value of 1.0. This is different than the default value of layer.borderWidth, which is 0. Somehow we need to get layer.borderWidth to match buttonBorderWidth when the view is initialized, even if buttonBorderWidth is never modified. Since the code that sets layer.borderWidth is in updateViews, we can just make sure we call updateViews at some point before the view is displayed (e.g. in init or in layoutSubviews or in willMove(toWindow:)).
If we want to make buttonBorderWidth be a computed property instead, we either have to force-set the buttonBorderWidth to its existing value somewhere, or duplicate the code that sets layer.borderWidth somewhere. That is, we either have to do something like this:
init(frame: CGRect) {
...
super.init(frame: frame)
// This is cumbersome because:
// - init won't call buttonBorderWidth.didSet by default.
// - You can't assign a property to itself, e.g. `a = a` is banned.
// - Without the semicolon, the closure is treated as a trailing
// closure on the above call to super.init().
;{ buttonBorderWidth = { buttonBorderWidth }() }()
}
Or we have to do something like this:
init(frame: CGRect) {
...
super.init(frame: frame)
// This is the same code as in buttonBorderWidth.didSet:
layer.borderWidth = buttonBorderWidth
}
And if we have a bunch of these properties that cover layer properties but have different default values, we have to do this force-setting or duplicating for each of them.
My solution to this is generally to not have a different default value for my inspectable property than for the property it covers. If we just let the default value of buttonBorderWidth be 0 (same as the default for layer.borderWidth), then we don't have to get the two properties in sync because they're never out-of-sync. So I would just implement buttonBorderWidth like this:
#IBInspectable var buttonBorderWidth: CGFloat {
get { return layer.borderWidth }
set { layer.borderWidth = newValue }
}
So, when would you want to use a stored property with an observer? One condition especially applicable to IBInspectable is when the inspectable properties do not map trivially onto existing layer properties.
For example, in iOS 11 and macOS 10.13 and later, CALayer has a maskedCorners property that controls which corners are rounded by cornerRadius. Suppose we want to expose both cornerRadius and maskedCorners as inspectable properties. We might as well just expose cornerRadius using a computed property:
#IBInspectable var cornerRadius: CGFloat {
get { return layer.cornerRadius }
set { layer.cornerRadius = newValue }
}
But maskedCorners is essentially four different boolean properties combined into one. So we should expose it as four separate inspectable properties. If we use computed properties, it looks like this:
#IBInspectable var isTopLeftCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMinXMinYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMinXMinYCorner) }
else { layer.maskedCorners.remove(.layerMinXMinYCorner) }
}
}
#IBInspectable var isBottomLeftCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMinXMaxYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMinXMaxYCorner) }
else { layer.maskedCorners.remove(.layerMinXMaxYCorner) }
}
}
#IBInspectable var isTopRightCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMaxXMinYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMaxXMinYCorner) }
else { layer.maskedCorners.remove(.layerMaxXMinYCorner) }
}
}
#IBInspectable var isBottomRightCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMaxXMaxYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMaxXMaxYCorner) }
else { layer.maskedCorners.remove(.layerMaxXMaxYCorner) }
}
}
That's a bunch of repetitive code. It's easy to miss something if you write it using copy and paste. (I don't guarantee that I got it correct!) Now let's see what it looks like using stored properties with observers:
#IBInspectable var isTopLeftCornerRounded = true {
didSet { updateMaskedCorners() }
}
#IBInspectable var isBottomLeftCornerRounded = true {
didSet { updateMaskedCorners() }
}
#IBInspectable var isTopRightCornerRounded = true {
didSet { updateMaskedCorners() }
}
#IBInspectable var isBottomRightCornerRounded = true {
didSet { updateMaskedCorners() }
}
private func updateMaskedCorners() {
var mask: CACornerMask = []
if isTopLeftCornerRounded { mask.insert(.layerMinXMinYCorner) }
if isBottomLeftCornerRounded { mask.insert(.layerMinXMaxYCorner) }
if isTopRightCornerRounded { mask.insert(.layerMaxXMinYCorner) }
if isBottomRightCornerRounded { mask.insert(.layerMaxXMaxYCorner) }
layer.maskedCorners = mask
}
I think this version with stored properties has several advantages over the version with computed properties:
The parts of the code that are repeated are much shorter.
Each mask option is only mentioned once, so it's easier to make sure the options are all correct.
All the code that actually computes the mask is in one place.
The mask is constructed entirely from scratch each time, so you don't have to know the mask's prior value to understand what its new value will be.
Here's another example where I'd use a stored property: suppose you want to make a PolygonView and make the number of sides be inspectable. We need code to create the path given the number of sides, so here it is:
extension CGPath {
static func polygon(in rect: CGRect, withSideCount sideCount: Int) -> CGPath {
let path = CGMutablePath()
guard sideCount >= 3 else {
return path
}
// It's easiest to compute the vertices of a polygon inscribed in the unit circle.
// So I'll do that, and use this transform to inscribe the polygon in `rect` instead.
let transform = CGAffineTransform.identity
.translatedBy(x: rect.minX, y: rect.minY) // translate to the rect's origin
.scaledBy(x: rect.width, y: rect.height) // scale up to the rect's size
.scaledBy(x: 0.5, y: 0.5) // unit circle fills a 2x2 box but we want a 1x1 box
.translatedBy(x: 1, y: 1) // lower left of unit circle's box is at (-1, -1) but we want it at (0, 0)
path.move(to: CGPoint(x: 1, y: 0), transform: transform)
for i in 1 ..< sideCount {
let angle = CGFloat(i) / CGFloat(sideCount) * 2 * CGFloat.pi
print("\(i) \(angle)")
path.addLine(to: CGPoint(x: cos(angle), y: sin(angle)), transform: transform)
}
path.closeSubpath()
print("rect=\(rect) path=\(path.boundingBox)")
return path
}
}
We could write code that takes a CGPath and counts the number of segments it draws, but it is simpler to just store the number of sides directly. So in this case, it makes sense to use a stored property with an observer that triggers an update to the layer path:
class PolygonView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
#IBInspectable var sideCount: Int = 3 {
didSet {
setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
(layer as! CAShapeLayer).path = CGPath.polygon(in: bounds, withSideCount: sideCount)
}
}
I update the path in layoutSubviews because I also need to update the path if the view's size changes, and a size change also triggers layoutSubviews.
First of all, what you are asking about is nothing to do with #IBInspectable or #IBDesignable. Those are just directives for XCode to use with the Interface Builder when you create your own View/ViewControllers. Any property with #IBInspectable also appears in the attributes inspector in the Interface Builder. And #IBDesignable is for displaying the custom view in Interface builder. Now to get to the didSet and get/set
didSet
This is what you call a Property Observer. You can define property observers for a stored property to monitor the changes in a property. There are 2 flavors to monitor the change willSet and didSetthat can be defined. So you define the observers to perform some block of code where there is a change to that property. If you define willSet that code will be called before the property is set. Likewise didSet is the block run after the property has been set. So depending on what you need to do you can implement either of the observers.
get/set
Besides stored properties you can define something called Computed properties. As the name implies computed properties do not create and store any values themselves. These values are computed when needed. So these properties need get and set code to compute the property when required. If there is only a get that means it’s a read only property.
Hope this helps. Read the Swift book and go through the first few lectures of CS193p on iTunesU
didSet means "do the following when the variable is set". In your case, if you change buttonBorderWidth, the function updateView() will be called.
get and set are what you actually get when you ask for the variable itself. If I set shadowOpacity, it will pass it on to the set code. If I get shadowOpacity, it will actually get me layer.shadowOpacity.
#IBInspectable var buttonBorderWidth: CGFloat = 1.0
In that example, buttonBorderWidth is an actual property of the view. The attributes inspector can write to it and read it directly. The didSet observer is just so that something happens in response to our changing that property.
That's totally different from the other example:
#IBInspectable
var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
In that example, the goal is to make the layer's shadowOpacity inspectable. But you can't do that, because it's not a property of the view. Therefore we put a façade in front of the layer property, in the form of a computed "property" of the view; the attributes inspector can't see layer.shadowOpacity, but it can see the view's shadowOpacity which, unbeknownst to it, is just a way of accessing the layer's shadowOpacity.

Animation in core graphics

I followed this awesome Rey Wenderlich tutorial to make an Bezier arc and increment/decrement values. But how to animate the arch instead of just step-up and step-down?
http://www.raywenderlich.com/90690/modern-core-graphics-with-swift-part-1
I tried putting animation block in custom property declaration, which I dont think is the right place to do it and xcode doesn't let me do it anyway.
#IBInspectable var counter: Int = 5 {
didSet {
if counter <= NoOfGlasses {
//the view needs to be refreshed
UIView.animateWithDuration(0.2, animations: {
setNeedsDisplay()
}, completion:nil
)
}
}
}
Also tried to put the increment in animation block in View controller, didn't work.
#IBAction func btnPushButton(sender: AnyObject) {
UIView.animateWithDuration(0.2, animations: {
self.arcView.counter = self.arcView.counter + 10
self.counterLabel.text = String(self.arcView.counter)
}, completion:nil
)
}
Are you describing this sort of thing?
That's a simpler example - it's just a drawn triangle - but it's the same idea, if I'm understanding you correctly: we are animating the difference between one drawing and another.
Basically you have two choices. The easy way is to use CAShapeLayer, which animates for you automatically when you change its path. The other choice is to do what I'm doing here, which is to create a custom animatable property - in this case, a property representing the x-position of the bottom point of the triangle.