Subclassed CALayer animation function does not animate - ios

Currently, I have subclassed CALayer to create a animatable bottom border for my UITextField. However, my animateColor function is not working. I've tested to see if the function is being called and it is. However, no animation is taking place. What am I doing wrong?
class KTextFieldBottomBorder:CALayer {
override init!() {
super.init()
commonInit()
}
override init!(layer: AnyObject!) {
super.init(layer: layer)
commonInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func commonInit() {
// Appearance
self.backgroundColor = UIColor.lightGrayColor().CGColor
}
// MARK: Animations
func animateColor (endingColor:UIColor, duration:CFTimeInterval) {
var colorAnimation = CABasicAnimation(keyPath: "color")
colorAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
colorAnimation.toValue = endingColor.CGColor
colorAnimation.duration = duration
colorAnimation.fillMode = kCAFillModeForwards
super.addAnimation(colorAnimation, forKey: "color")
println("X")
}
}

You need to define the property that shall be animated by setting the keyPath.
colorAnimation.keyPath = "backgroundColor"
The key argument of addAnimation is merely used as an identifier for the animation.

Related

Changing the background color of a custom UIButton?

I've been setting the backgroundColor of buttons by doing self.layer.backgroundColor = someColor.
However, this doesn't seem to work with a custom class? I have this general class that I use:
class DarkButton: BaseButton {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = 5
self.setTitleColor(UIColor.black, for: .normal)
self.setTitleColor(UIColor.lightGray, for: .highlighted)
self.layer.backgroundColor = UIColor(red:0.77, green:0.77,
blue:0.77, alpha: 1.0).cgColor
self.clipsToBounds = false
}
}
the self.layer.backgroundColor works just fine. Now if I extend it, like so:
class SuperCoolButton: DarkButton {
required init() {
super.init()
self.setUp()
}
required init(spacing: Spacing) {
super.init(spacing: spacing)
self.setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setUp()
}
func setUp() {
self.generateImage()
self.changeBGColor()
}
func generateImage() {
let image = UIImage(named: "logoSmall") as UIImage?
self.setImage(image, for: .normal)
self.imageEdgeInsets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
self.imageView?.contentMode = .scaleAspectFit
}
func changeBGColor() {
self.layer.backgroundColor = UIColor.red.cgColor
}
}
the backgroundColor stays that of the DarkButton backgroundColor, however setting the image does indeed work >_>
This is happening because layoutSubviews() implemented in the super class(DarkButton) gets called after setUp method of DarkButton. You need to override layoutSubviews in your class SuperCoolButton and call setUp there instead of at its init method.
override func layoutSubviews() {
super.layoutSubviews()
setUp()
}
EDIT:
I think you should move the code which you have written inside [layoutSubviews][1] of DarkButton class.
layoutSubviews method gets called multiple times and only the code related to the layout of the view should be there.
Subclasses can override this method as needed to perform more precise
layout of their subviews. You should override this method only if the
autoresizing and constraint-based behaviors of the subviews do not
offer the behavior you want.
The ideal place to change layer's corner radius and setTitleColor or background color is either at init of the custom view or at awakeFromNib:(only, if the view is always going to be designed in nib).
Because you want to change the backgroud color of button at some later time. You can simply call your changeBgColor method on SuperCoolButton. Earlier it was not working, because layoutSubviews must be setting the background color back to the default.
Do not call your setUp() method in init instead of this call it in layoutSubviews
class SuperCoolButton: DarkButton {
required init() {
super.init()
//self.setUp()
}
required init(spacing: Spacing) {
super.init(spacing: spacing)
//self.setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//self.setUp()
}
func setUp() {
self.generateImage()
self.changeBGColor()
}
func generateImage() {
let image = UIImage(named: "logoSmall") as UIImage?
self.setImage(image, for: .normal)
self.imageEdgeInsets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
self.imageView?.contentMode = .scaleAspectFit
}
func changeBGColor() {
self.layer.backgroundColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
setUp()
}
}

Add UIView rounding logic inside UIView class

I have a custom UIView and I add it to my ViewController like this:
let myCustomView = Bundle.main.loadNibNamed("MyCustomView", owner: nil, options: nil) as! MyCustomView
myCustomView.layer.cornerRadius = 10
myCustomView.layer.masksToBounds = true
I round the corners of the view. But I am wondering, is there a way to move this logic of rounding the corners inside the MyCustomView class?
As you use IB, you may find it more convenient to make an extension of UIView
extension UIView {
#IBInspectable var borderColor: UIColor? {
set {
layer.borderColor = newValue?.cgColor
}
get {
if let color = layer.borderColor {
return UIColor(cgColor:color)
} else {
return nil
}
}
}
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
clipsToBounds = newValue > 0
}
get {
return layer.cornerRadius
}
}
}
Then you can set those values from Attributes inspector.
Yes - If you're loading a nib with a custom view, that nib is very likely referring to another class. If that's the case, you can move the logic inside the class itself.
That said, I really like Lawliet's suggestion of making a UIView extension with IBInspectable properties. The downside to that approach is that every single view now has these properties, which creates a certain overhead and potential for clashes.
You can do something like this in your UIView subclass:
class RoundedView: UIView {
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
}
Also if you want to pass a custom value instead of '10' in the cornerRadius property, you can try to implement a convenience init by looking here:
Override Init method of UIView in Swift

Why can't I draw on an image when I subclass UIView?

I am trying to draw on an image from the camera roll using the code from this tutorial. After selecting a photo from the camera roll, imageView is set to the photo. When the user presses the draw button, tempImageView is added over the photo, so the user can draw onto it. However, when I attempt to draw on the image, nothing happens. Why?
Code:
class EditViewController: UIViewController{
var tempImageView: DrawableView!
var imageView: UIImageView = UIImageView(image: UIImage(named: "ClearImage"))
func draw(sender: UIButton!) {
tempImageView = DrawableView(frame: imageView.bounds)
tempImageView.backgroundColor = UIColor.clearColor()
self.view.addSubview(tempImageView)
}
}
class DrawableView: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint
var lineWidth:CGFloat=10.0
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override init(frame: CGRect) {
previousPoint=CGPoint.zero
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
previousPoint=CGPoint.zero
super.init(coder: aDecoder)
let panGestureRecognizer=UIPanGestureRecognizer(target: self, action: "pan:")
panGestureRecognizer.maximumNumberOfTouches=1
self.addGestureRecognizer(panGestureRecognizer)
}
override func drawRect(rect: CGRect) {
// Drawing code
UIColor.greenColor().setStroke()
path.stroke()
path.lineWidth=lineWidth
}
func pan(panGestureRecognizer:UIPanGestureRecognizer)->Void
{
let currentPoint=panGestureRecognizer.locationInView(self)
let midPoint=self.midPoint(previousPoint, p1: currentPoint)
if panGestureRecognizer.state == .Began
{
path.moveToPoint(currentPoint)
}
else if panGestureRecognizer.state == .Changed
{
path.addQuadCurveToPoint(midPoint,controlPoint: previousPoint)
}
previousPoint=currentPoint
self.setNeedsDisplay()
}
func midPoint(p0:CGPoint,p1:CGPoint)->CGPoint
{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}
make sure that tempImageView.userIteractionEnabled = YES. by default this property for UIImageView is NO.
after put breakpoint to you gestureRecogniser method, see if it's being called.

How does initializing UIView subclass properties affect superview's center?

I'm having a problem with the graphOrigin property in my UIView subclass. When I defined graphOrigin as a computed variable, it convert's the superview's center point to this view's center point and displays the graph in the center of the screen. This does not happen when the variable isn't computed. See code and screenshot for the working case:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint {
return convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes in the view
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
AxesDrawer is a class that draws axes in the current view, here is the method signature for drawAxesInRect:
drawAxesInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
And here is the code and screenshot for the case that doesn't work:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint! {
didSet {
setNeedsDisplay()
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
So literally all I changed was initializing the graphOrigin property in place and computing it in the initializer. I didn't touch the StoryBoard at all while editing this code.
I tried initializing the variable inline:
var graphOrigin = convertPoint(center, fromView: superview)
But this wasn't allowed because the implicit self is not initialized when properties are computed.
Can anyone explain why the superview's center seems to change location depending on how the variable is initialized?
This function
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
means that you are loading view from xib, and at this time the view dimension is 600:600 (look at your xib). So that your graphOrigin = 300:300. This is why you see the second picture.
To fix that problem, you should compute the graphOrigin after the view finish layout in viewDidLayout.

Core animation progress callback

Is there an easy way to be called back when a Core Animation reaches certain points as it's running (for example, at 50% and 66% of completion ?
I'm currently thinking about setting up an NSTimer, but that's not really as accurate as I'd like.
I've finally developed a solution for this problem.
Essentially I wish to be called back for every frame and do what I need to do.
There's no obvious way to observe the progress of an animation, however it is actually possible:
Firstly we need to create a new subclass of CALayer that has an animatable property called 'progress'.
We add the layer into our tree, and then create an animation that will drive the progress value from 0 to 1 over the duration of the animation.
Since our progress property can be animated, drawInContext is called on our sublass for every frame of an animation. This function doesn't need to redraw anything, however it can be used to call a delegate function :)
Here's the class interface:
#protocol TAProgressLayerProtocol <NSObject>
- (void)progressUpdatedTo:(CGFloat)progress;
#end
#interface TAProgressLayer : CALayer
#property CGFloat progress;
#property (weak) id<TAProgressLayerProtocol> delegate;
#end
And the implementation:
#implementation TAProgressLayer
// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it's animating.
- (id)initWithLayer:(id)layer
{
self = [super initWithLayer:layer];
if (self) {
TAProgressLayer *otherLayer = (TAProgressLayer *)layer;
self.progress = otherLayer.progress;
self.delegate = otherLayer.delegate;
}
return self;
}
// Override needsDisplayForKey so that we can define progress as being animatable.
+ (BOOL)needsDisplayForKey:(NSString*)key {
if ([key isEqualToString:#"progress"]) {
return YES;
} else {
return [super needsDisplayForKey:key];
}
}
// Call our callback
- (void)drawInContext:(CGContextRef)ctx
{
if (self.delegate)
{
[self.delegate progressUpdatedTo:self.progress];
}
}
#end
We can then add the layer to our main layer:
TAProgressLayer *progressLayer = [TAProgressLayer layer];
progressLayer.frame = CGRectMake(0, -1, 1, 1);
progressLayer.delegate = self;
[_sceneView.layer addSublayer:progressLayer];
And animate it along with the other animations:
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"progress"];
anim.duration = 4.0;
anim.beginTime = 0;
anim.fromValue = #0;
anim.toValue = #1;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[progressLayer addAnimation:anim forKey:#"progress"];
Finally, the delegate will be called back as the animation progresses:
- (void)progressUpdatedTo:(CGFloat)progress
{
// Do whatever you need to do...
}
If you don't want to hack a CALayer to report progress to you, there's another approach. Conceptually, you can use a CADisplayLink to guarantee a callback on each frame, and then simply measure the time that has passed since the start of the animation divided by the duration to figure out the percent complete.
The open source library INTUAnimationEngine packages this functionality up very cleanly into an API that looks almost exactly like the UIView block-based animation one:
// INTUAnimationEngine.h
// ...
+ (NSInteger)animateWithDuration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
animations:(void (^)(CGFloat percentage))animations
completion:(void (^)(BOOL finished))completion;
// ...
All you need to do is call this method at the same time you start other animation(s), passing the same values for duration and delay, and then for each frame of the animation the animations block will be executed with the current percent complete. And if you want peace of mind that your timings are perfectly synchronized, you can drive your animations exclusively from INTUAnimationEngine.
I made a Swift (2.0) implementation of the CALayer subclass suggested by tarmes in the accepted answer:
protocol TAProgressLayerProtocol {
func progressUpdated(progress: CGFloat)
}
class TAProgressLayer : CALayer {
// MARK: - Progress-related properties
var progress: CGFloat = 0.0
var progressDelegate: TAProgressLayerProtocol? = nil
// MARK: - Initialization & Encoding
// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it's animating.
override init(layer: AnyObject) {
super.init(layer: layer)
if let other = layer as? TAProgressLayerProtocol {
self.progress = other.progress
self.progressDelegate = other.progressDelegate
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
progressDelegate = aDecoder.decodeObjectForKey("progressDelegate") as? CALayerProgressProtocol
progress = CGFloat(aDecoder.decodeFloatForKey("progress"))
}
override func encodeWithCoder(aCoder: NSCoder) {
super.encodeWithCoder(aCoder)
aCoder.encodeFloat(Float(progress), forKey: "progress")
aCoder.encodeObject(progressDelegate as! AnyObject?, forKey: "progressDelegate")
}
init(progressDelegate: TAProgressLayerProtocol?) {
super.init()
self.progressDelegate = progressDelegate
}
// MARK: - Progress Reporting
// Override needsDisplayForKey so that we can define progress as being animatable.
class override func needsDisplayForKey(key: String) -> Bool {
if (key == "progress") {
return true
} else {
return super.needsDisplayForKey(key)
}
}
// Call our callback
override func drawInContext(ctx: CGContext) {
if let del = self.progressDelegate {
del.progressUpdated(progress)
}
}
}
Ported to Swift 4.2:
protocol CAProgressLayerDelegate: CALayerDelegate {
func progressDidChange(to progress: CGFloat)
}
extension CAProgressLayerDelegate {
func progressDidChange(to progress: CGFloat) {}
}
class CAProgressLayer: CALayer {
private struct Const {
static let animationKey: String = "progress"
}
#NSManaged private(set) var progress: CGFloat
private var previousProgress: CGFloat?
private var progressDelegate: CAProgressLayerDelegate? { return self.delegate as? CAProgressLayerDelegate }
override init() {
super.init()
}
init(frame: CGRect) {
super.init()
self.frame = frame
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.progress = CGFloat(aDecoder.decodeFloat(forKey: Const.animationKey))
}
override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(Float(self.progress), forKey: Const.animationKey)
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == Const.animationKey { return true }
return super.needsDisplay(forKey: key)
}
override func display() {
super.display()
guard let layer: CAProgressLayer = self.presentation() else { return }
self.progress = layer.progress
if self.progress != self.previousProgress {
self.progressDelegate?.progressDidChange(to: self.progress)
}
self.previousProgress = self.progress
}
}
Usage:
class ProgressView: UIView {
override class var layerClass: AnyClass {
return CAProgressLayer.self
}
}
class ExampleViewController: UIViewController, CAProgressLayerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let progressView = ProgressView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
progressView.layer.delegate = self
view.addSubview(progressView)
var animations = [CAAnimation]()
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0
opacityAnimation.toValue = 1
opacityAnimation.duration = 1
animations.append(opacityAnimation)
let progressAnimation = CABasicAnimation(keyPath: "progress")
progressAnimation.fromValue = 0
progressAnimation.toValue = 1
progressAnimation.duration = 1
animations.append(progressAnimation)
let group = CAAnimationGroup()
group.duration = 1
group.beginTime = CACurrentMediaTime()
group.animations = animations
progressView.layer.add(group, forKey: nil)
}
func progressDidChange(to progress: CGFloat) {
print(progress)
}
}

Resources