I have a done button with a IBAction as following:
#IBAction func done() {
print("Done pressed");
let hudView = HudView.hud(inView: (navigationController?.view)!, animated: true)
print("Set text");
hudView.text = "Toggled"
}
The HudView is a subclass of UIView has a convinience constructor
class func hud(inView view: UIView, animated: Bool) -> HudView
{
print("Constructor is called")
let hudView = HudView(frame: view.bounds)
hudView.isOpaque = false
view.addSubview(hudView)
view.isUserInteractionEnabled = false
return hudView
}
And also I overwrite the draw method in it.
override func draw(_ rect: CGRect) {
print("Draw is called")
...
}
The problem is that I don't understand why it prints out in such sequence
Done pressed
Constructor is called
Set text
Draw is called
Why is the draw method not called until I set the text? Why not immediately after it is constructed?
According to Apple's Official Documentation.
This method is called when a view is first displayed or when an event
occurs that invalidates a visible part of the view. You should never
call this method directly yourself. To invalidate part of your view,
and thus cause that portion to be redrawn, call the setNeedsDisplay()
or setNeedsDisplay(_:) method instead.
When you initialise the view, it's not added to the view hierarchy and display for that view is not needed. So draw(_:) is not called. When you added the view as subview to the view hierarchy, it has to display and the method is called.
Somethings other than setNeedsDisplay() and setNeedsDisplay(_:) that trigger this method are when you make layouts to the view or change the size of the view.
I'm a newbie to the iOS development and I'm trying to figure out how CATiledLayer works. NOT how to use it!
There are two key features of CATiledLayer I want to know how it works.
Visible bounds automatic reporting. How does CATiledLayer know its visible bounds have changed? Can I create a custom CALayer and register to know the same information?
Tile drawing. How are these tiles drawn? Are they drawn as sublayers?
The reason I want to find out how these two key points work is that I'm building a custom UIView that can have a very large width and a CALayer will crash due to having such a large backed layer. CATiledLayer has a light memory usage and its' almost perfect!
The reason I don't want to use CATiledLayer is how it works internally. All drawing happens on a background thread. I know you can even have the drawing logic be called on the main thread but this would happen on the next drawing cycle. Because drawing doesn't happen on the same draw cycle, during a view resize our drawing logic has a delay updating the drawings thus causing the UIView content to shake during updates by the user.
Just to add a little more to this. I'm building an audio editor where it shows the audio waveform and the user can resize this clip. The clip is shown inside a collection view. The mentioned issue above with CATiledLayer seems to be the same issue Garage band has. I can barely notice it when resizing an audio clip to the left. They are likely using CATiledLayer.
I know one way to do question number one but I am not sure of the consequences and efficiency. This is more theoretical except it works. I am using a CADisplayLink to run a check to see if the frame of the layer is in the main window. I did notice a small bit of CPU (1% or less) being used so I would test it more compared to the CATiledLayer. CATiledLayer just breaks the drawing up but operates on the same premise that only what is visible can be drawn. drawRect I think fundamentally works when visible or the visible bounds change. As far as subclass I tested I used it inside a UICollectionView and know that it works. I could even get logs of when a cell was created and not on screen. Here is the working subclass of CALayer. I don't know if this helps you but it is possible.
import UIKit
protocol OnScreenLayerDelegate:class {
func layerIsOnScreen(currentScreenSpace:CGRect)
func layerIsNotOnScreen(currentScreenSpace:CGRect)
}
class OnScreenLayer: CALayer {
var displayLink : CADisplayLink?
weak var onScreenDelegate : OnScreenLayerDelegate?
override init() {
super.init()
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
func commonSetup(){
displayLink = CADisplayLink(target: self, selector: #selector(checkOnScreen))
displayLink?.add(to: .main, forMode: .commonModes)
}
#objc func checkOnScreen(){
if let window = UIApplication.shared.keyWindow{
let currentRect = self.convert(self.bounds, to: window.layer)
if window.bounds.intersects(currentRect){
onScreenDelegate?.layerIsOnScreen(currentScreenSpace: currentRect)
}else{
onScreenDelegate?.layerIsNotOnScreen(currentScreenSpace: currentRect)
}
}
}
}
As to question 2 I think CATiledLayers probably does not use sublayers and instead slowly draws all the contents into a single contents image but by tiling and probably easier math for them. It might be something that takes the visible area draws it in the background and provides the layer contents. Then caches that section and adds another until it is complete.This is only a guess.
Here is the code from my test in a collectionView cell.
import UIKit
class AlbumCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
var onScreenLayer = OnScreenLayer()
var currentIndex : Int = 0
override func awakeFromNib() {
super.awakeFromNib()
onScreenLayer.frame = self.bounds
self.layer.addSublayer(onScreenLayer)
onScreenLayer.onScreenDelegate = self
}
override func layoutSubviews() {
super.layoutSubviews()
onScreenLayer.frame = self.bounds
}
}
extension AlbumCollectionViewCell : OnScreenLayerDelegate{
func layerIsOnScreen(currentScreenSpace: CGRect) {
//or more proof
print("it is on screen \(currentIndex)")
}
func layerIsNotOnScreen(currentScreenSpace: CGRect) {
print("not on screen \(currentIndex)")
}
}
The current index was set in cellForItem so I could monitor it. Let me know if this helps. Also the check could modify the frame to catch it right before it comes on screen by a margin that way you are drawing prior to that.
Preamble (page down after code for the actual problem):
I have a custom UIButton class in which I replaced the ordinary UIButton isHighlighted animation behavior with this behavior:
When the user's finger is actually on the button (ie. highlighting the button), a stroked outline (square border) will appear around the button. When they are no longer touching the button, the outline disappears.
Also, I have replaced the normal isSelected behavior (background goes blue) with this:
An outline identical to the one used with isHighlighted is drawn, except it is slightly thicker and always there as long as isSelected = true, and it is not there when isSelected = false.
This works, and this is how I did it:
import UIKit
class CustomButton: UIButton {
// BUTTON TYPES:
// Gorsh, enums sure would be swell here. :T
// 1 : plus
// 2 : minus
// 3 : etc etc....
#IBInspectable
var thisButtonsType: Int = 1 { didSet { setNeedsDisplay() } }
#IBInspectable
var fillColor: UIColor = .black { didSet { setNeedsDisplay() } }
#IBInspectable
var strokeColor: UIColor = .black { didSet { setNeedsDisplay() } }
override var isHighlighted: Bool {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let unitOfMeasure = bounds.width / 5
// draw outline in case we are selected / highlighted
if (isSelected || isHighlighted) {
let tempPath = BezierPathFactory.outline(totalWidth: bounds.width)
if (isSelected) {tempPath.lineWidth = 4.0}
strokeColor.setStroke()
tempPath.stroke()
}
// initialize path object
var path = UIBezierPath()
// get the path corresponding to our button type
switch thisButtonsType {
case 1:
path = BezierPathFactory.plus(gridSpacing: unitOfMeasure)
case 2:
path = BezierPathFactory.minus(gridSpacing: unitOfMeasure)
default: print("hewo!")
}
fillColor.setFill()
path.fill()
}
}
Once again, that code works but ...
What I would really like is if that isHighlighted border outline would gradually fade away after the touch finishes.
Note: In case you're wondering why I would do things this way... I plan to implement lots of different button types... all with different icons "printed" on them... but I want them all to follow these basic behaviors. Only a couple of them are "toggle" buttons that ever become selected, so there IS a use for the highlight fade.
I already know how to do animations... but it's just doing my head in trying to think how to do that in this context.
I wish I could get the UIButton source code.
Is there a way that the fade animation could be added relatively simply to my above code without completely changing the design pattern? Am I going to have to start using some other feature of core graphics like layers?
Thanks!
You might do the following:
Create a subclass of UIButton which adds and manages a sublayer of class CAShapeLayer for the outline the button.
Draw the outline the outline into the shape layer.
Ensure that the layer resizes with the view: Overwrite layoutSubviews of your view such that it updates the frame of the layer and updates the path for the outline.
You should observe or overwrite the state property of UIControl (which is an ancestor of UIButton) to notice the state changes.
Use property animations (CABasicAnimation) for the opacity of the shape layer to fade the outline in and out.
I have a custom UIView that draws its contents using Core Graphics calls. All working well, but now I want to animate a change in value that affects the display. I have a custom property to achieve this in my custom UView:
var _anime: CGFloat = 0
var anime: CGFloat {
set {
_anime = newValue
for(gauge) in gauges {
gauge.animate(newValue)
}
setNeedsDisplay()
}
get {
return _anime
}
}
And I have started an animation from the ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.emaxView.anime = 0.5
UIView.animate(withDuration: 4) {
DDLogDebug("in animations")
self.emaxView.anime = 1.0
}
}
This doesn't work - the animated value does change from 0.5 to 1.0 but it does so instantly. There are two calls to the anime setter, once with value 0.5 then immediately a call with 1.0. If I change the property I'm animating to a standard UIView property, e.g. alpha, it works correctly.
I'm coming from an Android background, so this whole iOS animation framework looks suspiciously like black magic to me. Is there any way of animating a property other than predefined UIView properties?
Below is what the animated view is supposed to look like - it gets a new value about every 1/2 second and I want the pointer to move smoothly over that time from the previous value to the next. The code to update it is:
open func animate(_ progress: CGFloat) {
//DDLogDebug("in animate: progress \(progress)")
if(dataValid) {
currentValue = targetValue * progress + initialValue * (1 - progress)
}
}
And calling draw() after it's updated will make it redraw with the new pointer position, interpolating between initialValue and targetValue
Short answer: use CADisplayLink to get called every n frames. Sample code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.preferredFramesPerSecond = 50
displayLink.add(to: .main, forMode: .defaultRunLoopMode)
updateValues()
}
var animationComplete = false
var lastUpdateTime = CACurrentMediaTime()
func updateValues() {
self.emaxView.animate(0);
lastUpdateTime = CACurrentMediaTime()
animationComplete = false
}
func animationDidUpdate(displayLink: CADisplayLink) {
if(!animationComplete) {
let now = CACurrentMediaTime()
let interval = (CACurrentMediaTime() - lastUpdateTime)/animationDuration
self.emaxView.animate(min(CGFloat(interval), 1))
animationComplete = interval >= 1.0
}
}
}
The code could be refined and generalised but it's doing the job I needed.
You will need to call layoufIfNeeded() instead of setNeedsDisplay() if you modify any auto layout constraints in your gauge.animate(newValue) function.
https://stackoverflow.com/a/12664093/255549
If that is drawn entirely with CoreGraphics there is a pretty simple way to animate this if you want to do a little math. Fortunately you have a scale there that tells you the number of radians exactly to rotate, so the math is minimal and no trigonometry is involved. The advantage of this is you won't have to redraw the entire background, or even the pointer. It can be a bit tricky to get angles and stuff right, I can help out if the following doesn't work.
Draw the background of the view normally in draw(in rect). The pointer you should put into a CALayer. You can pretty much just move the draw code for the pointer, including the centre dark gray circle into a separate method that returns a UIImage. The layer will be sized to the frame of the view (in layout subviews), and the anchor point has to be set to (0.5, 0.5), which is actually the default so you should be ok leaving that line out. Then your animate method just changes the layer's transform to rotate according to what you need. Here's how I would do it. I'm going to change the method and variable names because anime and animate were just a bit too obscure.
Because layer properties implicitly animate with a duration of 0.25 you might be able to get away without even calling an animation method. It's been a while since I've worked with CoreAnimation, so test it out obviously.
The advantage here is that you just set the RPM of the dial to what you want, and it will rotate over to that speed. And no one will read your code and be like WTF is _anime! :) I have included the init methods to remind you to change the contents scale of the layer (or it renders in low quality), obviously you may have other things in your init.
class SpeedDial: UIView {
var pointer: CALayer!
var pointerView: UIView!
var rpm: CGFloat = 0 {
didSet {
pointer.setAffineTransform(rpm == 0 ? .identity : CGAffineTransform(rotationAngle: rpm/25 * .pi))
}
}
override init(frame: CGRect) {
super.init(frame: frame)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
//draw background with values
//but not the pointer or centre circle
context.restoreGState()
}
override func layoutSubviews() {
super.layoutSubviews()
pointerView.frame = bounds
pointer.frame = bounds
pointer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pointer.contents = drawPointer(in: bounds)?.cgImage
}
func drawPointer(in rect: CGRect) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
// draw the pointer Image. Make sure to draw it pointing at zero. ie at 8 o'clock
// I'm not sure what your drawing code looks like, but if the pointer is pointing
// vertically(at 12 o'clock), you can get it pointing at zero by rotating the actual draw context like so:
// perform this context rotation before actually drawing the pointer
context.translateBy(x: rect.width/2, y: rect.height/2)
context.rotate(by: -17.5/25 * .pi) // the angle judging by the dial - remember .pi is 180 degrees
context.translateBy(x: -rect.width/2, y: -rect.height/2)
context.restoreGState()
let pointerImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return pointerImage
}
}
The pointer's identity transform has it pointing at 0 RPM, so every time you up the RPM to what you want, it will rotate up to that value.
edit: tested it, it works. Except I made a couple errors - you don't need to change the layers position, I updated the code accordingly. Also, changing the layer's transform triggers layoutSubviews in the immediate parent. I forgot about this. The easiest way around this is to put the pointer layer into a UIView that is a subview of SpeedDial. I've updated the code. Good luck! Maybe this is overkill, but its a bit more reusable than animating the entire rendering of the view, background and all.
I have a subclassed UIView that we can call CircleView. CircleView automatically sets a corner radius to half of its width in order for it to be a circle.
The problem is that when "CircleView" is resized by an AutoLayout constraint... for example on a device rotation... it distorts badly until the resize takes place because the "cornerRadius" property has to catch up, and the OS only sends a single "bounds" change to the view's frame.
I was wondering if anyone had a good, clear strategy for implementing "CircleView" in a way that won't distort in such instances, but will still mask its contents to the shape of a circle and allow for a border to exist around said UIView.
UPDATE: If your deployment target is iOS 11 or later:
Starting in iOS 11, UIKit will animate cornerRadius if you update it inside an animation block. Just set your view's layer.cornerRadius in a UIView animation block, or (to handle interface orientation changes), set it in layoutSubviews or viewDidLayoutSubviews.
ORIGINAL: If your deployment target is older than iOS 11:
So you want this:
(I turned on Debug > Slow Animations to make the smoothness easier to see.)
Side rant, feel free to skip this paragraph: This turns out to be a lot harder than it should be, because the iOS SDK doesn't make the parameters (duration, timing curve) of the autorotation animation available in a convenient way. You can (I think) get at them by overriding -viewWillTransitionToSize:withTransitionCoordinator: on your view controller to call -animateAlongsideTransition:completion: on the transition coordinator, and in the callback you pass, get the transitionDuration and completionCurve from the UIViewControllerTransitionCoordinatorContext. And then you need to pass that information down to your CircleView, which has to save it (because it hasn't been resized yet!) and later when it receives layoutSubviews, it can use it to create a CABasicAnimation for cornerRadius with those saved animation parameters. And don't accidentally create an animation when it's not an animated resize… End of side rant.
Wow, that sounds like a ton of work, and you have to involve the view controller. Here's another approach that's entirely implemented inside CircleView. It works now (in iOS 9) but I can't guarantee it'll always work in the future, because it makes two assumptions that could theoretically be wrong in the future.
Here's the approach: override -actionForLayer:forKey: in CircleView to return an action that, when run, installs an animation for cornerRadius.
These are the two assumptions:
bounds.origin and bounds.size get separate animations. (This is true now but presumably a future iOS could use a single animation for bounds. It would be easy enough to check for a bounds animation if no bounds.size animation were found.)
The bounds.size animation is added to the layer before Core Animation asks for the cornerRadius action.
Given these assumptions, when Core Animation asks for the cornerRadius action, we can get the bounds.size animation from the layer, copy it, and modify the copy to animate cornerRadius instead. The copy has the same animation parameters as the original (unless we modify them), so it has the correct duration and timing curve.
Here's the start of CircleView:
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
Note that the view's bounds are set before the view receives layoutSubviews, and therefore before we update cornerRadius. This is why the bounds.size animation is installed before the cornerRadius animation is requested. Each property's animations are installed inside the property's setter.
When we set cornerRadius, Core Animation asks us for a CAAction to run for it:
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
}
return super.action(for: layer, forKey: event)
}
In the code above, if we're asked for an action for cornerRadius, we look for a CABasicAnimation on bounds.size. If we find one, we copy it, change the key path to cornerRadius, and save it away in a custom CAAction (of class Action, which I will show below). We also save the current value of the cornerRadius property, because Core Animation calls actionForLayer:forKey: before updating the property.
After actionForLayer:forKey: returns, Core Animation updates the cornerRadius property of the layer. Then it runs the action by sending it runActionForKey:object:arguments:. The job of the action is to install whatever animations are appropriate. Here's the custom subclass of CAAction, which I've nested inside CircleView:
private class Action: NSObject, CAAction {
var pendingAnimation: CABasicAnimation?
var priorCornerRadius: CGFloat = 0
public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
if let layer = anObject as? CALayer, let pendingAnimation = pendingAnimation {
if pendingAnimation.isAdditive {
pendingAnimation.fromValue = priorCornerRadius - layer.cornerRadius
pendingAnimation.toValue = 0
} else {
pendingAnimation.fromValue = priorCornerRadius
pendingAnimation.toValue = layer.cornerRadius
}
layer.add(pendingAnimation, forKey: "cornerRadius")
}
}
}
} // end of CircleView
The runActionForKey:object:arguments: method sets the fromValue and toValue properties of the animation and then adds the animation to the layer. There's a complication: UIKit uses “additive” animations, because they work better if you start another animation on a property while an earlier animation is still running. So our action checks for that.
If the animation is additive, it sets fromValue to the difference between the old and new corner radii, and sets toValue to zero. Since the layer's cornerRadius property has already been updated by the time the animation is running, adding that fromValue at the start of the animation makes it look like the old corner radius, and adding the toValue of zero at the end of the animation makes it look like the new corner radius.
If the animation is not additive (which doesn't happen if UIKit created the animation, as far as I know), then it just sets the fromValue and toValue in the obvious way.
Here's the whole file for your convenience:
import UIKit
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
}
return super.action(for: layer, forKey: event)
}
private class Action: NSObject, CAAction {
var pendingAnimation: CABasicAnimation?
var priorCornerRadius: CGFloat = 0
public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
if let layer = anObject as? CALayer, let pendingAnimation = pendingAnimation {
if pendingAnimation.isAdditive {
pendingAnimation.fromValue = priorCornerRadius - layer.cornerRadius
pendingAnimation.toValue = 0
} else {
pendingAnimation.fromValue = priorCornerRadius
pendingAnimation.toValue = layer.cornerRadius
}
layer.add(pendingAnimation, forKey: "cornerRadius")
}
}
}
} // end of CircleView
My answer was inspired by this answer by Simon.
This answer builds upon the earlier answer by rob mayoff. Basically, I implemented it for our project and it worked just fine on the iPhone (iOS 9 and 10), but the issue remained on iPad (iOS 9 or 10).
Debugging, I found that the if statement:
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
always failed on iPad. It looks like the animations are built in a different sequence on iPad than iPhone. Looking back at the original answer by Simon, it seems that sequencing has changed before. So I combined both answers giving me something like this:
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
let buildAction: (CABasicAnimation) -> Action = { boundsAnimation in
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
return buildAction(boundsAnimation)
} else if let boundsAnimation = self.action(for: layer, forKey: "bounds") as? CABasicAnimation {
return buildAction(boundsAnimation)
}
}
return super.action(for: layer, forKey: event)
}
By combining both answers, it seems to work properly on both iPhone and iPad under iOS 9 and 10. I haven't really tested further, and don't know enough about CoreAnimation to fully understand this change.
In iOS 10 you don't need to create a CAAction, it works just creating a CABasicAnimation and provide this in your action(for layer:, for key:) -> CAAction? function (See Swift example):
private var currentBoundsAnimation: CABasicAnimation? {
return layer.animation(forKey: "bounds.size") as? CABasicAnimation ?? layer.animation(forKey: "bounds") as? CABasicAnimation
}
override public var bounds: CGRect {
didSet {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
}
override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
if(event == "cornerRadius"), let boundsAnimation = currentBoundsAnimation {
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.duration = boundsAnimation.duration
animation.timingFunction = boundsAnimation.timingFunction
return animation
}
return super.action(for: layer, forKey: event)
}
Instead of overriding the bounds property you can also override the layoutSubviews:
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
This works magically because the CABasicAnimation infers the missing from and to values from the model and presentation layers. To set the timing correctly you need the private currentBoundsAnimation property to get the current animations ("bounds" for iPad and "bounds.size" for iPhone) which where added on device rotation.
These translation answers usually go Objective-c ==> Swift, but in case there are any more stubborn Objective-c authors left, here's #Rob's answer translated...
// see https://stackoverflow.com/a/35714554/294949
#import "RoundView.h"
#interface Action : NSObject<CAAction>
#property(strong,nonatomic) CABasicAnimation *pendingAnimation;
#property(assign,nonatomic) CGFloat priorCornerRadius;
#end
#implementation Action
- (void)runActionForKey:(NSString *)event object:(id)anObject
arguments:(nullable NSDictionary *)dict {
if ([anObject isKindOfClass:[CALayer self]]) {
CALayer *layer = (CALayer *)anObject;
if (self.pendingAnimation.isAdditive) {
self.pendingAnimation.fromValue = #(self.priorCornerRadius - layer.cornerRadius);
self.pendingAnimation.toValue = #(0);
} else {
self.pendingAnimation.fromValue = #(self.priorCornerRadius);
self.pendingAnimation.toValue = #(layer.cornerRadius);
}
[layer addAnimation:self.pendingAnimation forKey:#"cornerRadius"];
}
}
#end
#interface RoundView ()
#property(weak,nonatomic) UIImageView *imageView;
#end
#implementation RoundView
- (void)layoutSubviews {
[super layoutSubviews];
[self updateCornerRadius];
}
- (void)updateCornerRadius {
self.layer.cornerRadius = MIN(self.bounds.size.width, self.bounds.size.height)/2.0;
}
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if ([event isEqualToString:#"cornerRadius"]) {
CABasicAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:#"bounds.size"];
CABasicAnimation *animation = [boundsAnimation copy];
animation.keyPath = #"cornerRadius";
Action *action = [[Action alloc] init];
action.pendingAnimation = animation;
action.priorCornerRadius = layer.cornerRadius;
return action;
}
return [super actionForLayer:layer forKey:event];;
}
#end
I would suggest not using a corner radius, but instead using a CAShapeLayer as a mask for your view's content layer.
You'd install a filled 360° arc CGPath as the shape of the shape layer and set it as the mask of your layer's view.
You could then either animate a new scale transform for the mask layer, or animate a change to the radius of the path. Both methods should stay round, although the scale transform might not give you a clean shape at smaller pixel sizes.
The timing would be the tricky part (getting the animation of the mask layer to happen in lockstep with the bounds animation.)