ios: Animating UIControl with CoreGraphics - ios

I'm following this tutorial and trying to make a slider.
I have a Slide class:
class YDSlider: UIControl {
var value = 0.0
...
And it has a sublayer with gradient to show the progress (just like a tutorial). I'd like to implement a func setValue(value: Double, animated: Bool) function, but I don't understand how to animate changes. My current approach is not working:
UIView.animateWithDuration(1, delay:1.0, options:UIViewAnimationOptions.LayoutSubviews, animations: { () -> Void in
self.value = value
}, completion: nil)

Without the source of your class, it will be difficult to help you but as far as I can see in the tutorial, the slider is designed with UIViews, CALayers and with the drawRect method.
So depending on the kind of elements of your slider you want to animate, you will have to use UIView animateions and for CoreAnimation.
If you need to animate parts designed with the drawRect method, this will be trickier and you will have to redraw manually the parts for each animation frame.

Related

Trouble restraining subview's animation within an animation block

Why restrain a subview from animation? Why not simply carry out its layout changes after the animation?
That's logical but not feasible considering my view hierarchy and this particular use-case.
My view hierarchy:
MyViewController: UIViewController -> MyCustomView: UIView -> MyCustomScrollView: UIScrollView, UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource
Why is it not possible?:
1) I do this in MyViewController after various constraint changes:
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
2) Since MyCustomView is a subview which contains MyCustomScrollView (which in turn contains a UICollectionView as its subview), the layout update triggers CV's willDisplay delegate method under which I'm adding a bunch of labels to MyCustomView, to be precise.
Here's the function in MyCustomView that I'm calling:
func addLabel(forIndexPath indexPath: IndexPath) {
var label: UILabel!
label.frame = Util.labelFrame(forIndex: indexPath, fillWidth: false) // The frame for the label is generated here!
//Will assign text and font to the label which are unnecessary to this context
self.anotherSubView.addSubview(label) //Add the label to MyCustomView's subview
}
3) Since these changes get caught up within the animation block from point 1, I get some unnecessary, undesired animations happening. And so, MyCustomView's layout change is bound with this animation block, forcing me to look for a way to restrain this from happening
Things tried so far:
1) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside a UIView.performWithoutAnimation {} block. - No luck
2) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside another animation block with 0.0 seconds time as to see if this overrides the parent animation block - No luck
3) Explored UIView.setAnimationsEnabled(enabled:) but it seems like this won't cancel/pause the existing animators, and will completely disable all the animations if true (which is not what I want)
To sum this all up, my problem is:
I need to restrain the animations on MyCustomView but I need all my other desired layout changes to take place. Is this even possible? Would really appreciate a hint or a solution, TYIA!
Thanks to this answer, removing all the animations from anotherSubview's layer(inside the addLabel(forIndexPath:)) after adding the label:
self.anotherSubview.addSubview(label)
self.anotherSubview.layer.removeAllAnimations() //Doing this removes the animations queued to animate the label's frame into the view
does exactly what I want!

Hide one UIView and show another

When I click on an a segment of my UISegmentedControl, I want one of two UIViews to be displayed. But how do I arrange it, that I only show the new view. Currently I am calling thisview.removeFromSuperview() on the old one, and then setup the new all from scratch. I also tried setting all the HeightConstants of the views subviews to zero and then set the heightConstants of the view itself to zero but I'd rather avoid that constraints-surgery..
What better approaches are there?
Agree with #rmaddy about using UIView's hidden property, a nice simple way to cause a view to not be drawn but still occupy its place in the view hierarchy and constraint system.
You can achieve a simple animation to make it a bit less jarring as follows:
UIView.animate(withDuration:0.4, animations: {
myView.alpha = 0
}) { (result: Bool) in
myView.isHidden = true
}
This will fade the alpha on the view "myView", then upon completion set it to hidden.
The same animation concept can be used also if you've views need to re-arrange themselves, animating layout changes will be a nice touch.
Based on #rmaddy and #CSmiths answer, I built the following function:
func changeView(newView: UIView, oldView: UIView) {
newView.isHidden = false
newView.alpha = 0
UIView.animate(withDuration:0.4, animations: {
oldView.alpha = 0
newView.alpha = 1
}) { (result: Bool) in
oldView.isHidden = true
}
}
I feel dumb now for all the hours I spent on that constraint-surgery. :|

Animated layout change with simultaneous view cross-dissolve

I have a UIView containing a number of UILabel positioned with AutoLayout.
On device rotation I switch the layout constraints to animate the labels to new locations (the layout constraints are derived from a Layout object which is assigned to the view):
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.viewWillTransition(to: size, with: coordinator)
let isLandscape = size.width > size.height
let layout = isLandscape ? layoutLandscape : layoutPortrait
coordinator.animate(
alongsideTransition: { _ in self.layoutView.setLayout(layout) },
completion: nil
)
}
This effect works well and provides a smooth transition between different content layouts optimised for landscape and portrait.
However, during the transition, the size of the labels also changes due to the layout constraints. It is noticeable that while the transition of labels' positions is gradual, the labels' shapes change immediately at the start of the transition, and then they animate to their final position.
I would like to be able to cross-disolve the labels between their initial and final shapes and have this occur alongside the position animation.
Ideally, I'd like to do this in the same highly decoupled manner that position animation is achieved, with the layout update code being oblivious of whether the change will be animated or not. I'm expecting at least a bit of compromise may be required, though!
Thank you.
I'm fairly close to a working solution on this. It's not answer material yet, so I'm going to put notes here as "Stuff I've Tried" – if it becomes an answer I'll turn it in to one.
A fantastic article at objc.io shows how you can create a UILabel subclass that will tell its backing CALayer to animate changes to its content property using a CATransition. It's purest unadulterated sorcery, but marvellously, it actually all makes sense and you suspect that perhaps you too can don the wizard's hat.
So, you end up with something a bit like this:
class ContentAnimatedLabel: UILabel {
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
guard event == "contents" else {
return super.action(for: layer, forKey: event)
}
let transition = CATransition()
// Oh noes! A hard coded made up value!
// And where do we get the timing curve?! Curses.
transition.duration = 0.3
return transition
}
}
And as far as it goes, it works! The content of the labels fades nicely, and their position interpolates. And it looks fairly grand.
The problem is that you this code doesn't know if it is being called from within an animation block. Really, it should only return the transition when being called from within an animation block, and it should copy animation properties such as duration and timing from those given by the animation block.
The article suggests a solution to this…
The UIView animation mechanism implementation is private, so there's no simple flag we can check to see if the view is currently animating, however we do know one thing: When animating, UIView's actionForLayer:forKey: will return valid CAActions for its animatable property keys, and when not animating it will return [NSNull null] for them. If we simply pick a suitable key we can interrogate UIView to see if it's currently supplying an action for that key, and use that to determine our response for our custom key. We'll use the key "backgroundColor" since that's a property of UIView that normally supports animation, and that we know returns a CABasicAnimation as its action (as of iOS 8, some properties, such as "bounds" return a private class instead, so watch out):
… but this doesn't seem to work, at least for the iOS 11 on which I'm testing.
When UIKit asks for the action for the content key, its as if the animation block has finished. Asking super for the action in use by another keys always returns nothing. So you're left with nothing to grab appropriate transition parameters from.

Get transition percentage in UINavigationController

I've two UIViews presented in the same UINavigationController; View A & View B.
During the transition, because I want to apply some custom animation styles, I want to know the percentage of the progress of the presentation inside the UINavigationController. How do I do this?
I want a function like this:
class ExampleNavigationController: UINavigationController {
func viewControllerMoves(from: UIViewController, to: UIViewController, progress: CGFloat) {
//I can read here how far the animation is and apply custom animation styles
}
}
This function should also be called during an update by the user:
Example --> http://www.latet.nl/appvideo.mov
How do I do this?
Example screens
Percentage would be 0.50
Percentage would be 1.00
Percentage would be 0.00
The UIViewControllerTransitionCoordinator protocol provides exactly what you need. In UIViewControllerTransitionCoordinatorContext, you will find a percentComplete that works for interactive transitions. For non-interactive transitions, it has all you need to animate alongside them.

How do I use animationWithDuration and Chameleon Gradients?

func fadeLightBar(){
UIView.animateWithDuration(fadeTime, delay: 0, options: UIViewAnimationOptions.AllowUserInteraction, animations: { [unowned self] () -> Void in
//self.lightBar.backgroundColor = RandomFlatColor()
self.lightBar.backgroundColor = GradientColor(UIGradientStyle.LeftToRight, self.lightBar.frame, [RandomFlatColor(), RandomFlatColor()])
}) { (stuff Bool) -> Void in
}
}
I want to fade a UIView from color to color. But, I want the UIView to be gradient color.
Doing the standard color-to-color works great. However, when I do the gradient, there is no fading at all. It just changes abruptly every X seconds.
I'm using this framework to generate gradients: https://github.com/ViccAlexander/Chameleon#gradient-colors-1
The framework works in its way, not simply change the color. I think it manipulate the layer by adding a CAGradientLayer on it. If so, it is impossible to animate adding a layer.
If you want to actualize the fading, you actually can do it yourself by adding a CAGradientLayer and animate its alpha

Resources