CABasicAnimation not reliable in UIStackView? - ios

I have a UIStackView with axis vertical that is displaying some items similar to a UITableView. However, I am animating them in. Delay using DispatchQueue.main.asyncAfter.
Here is the code I am using to create the CABasicAnimation:
let anim = CABasicAnimation(keyPath: "transform.translation.x")
anim.duration = 0.2
anim.fromValue = UIScreen.main.bounds.size.width
anim.toValue = 0
anim.isRemovedOnCompletion = true
anim.fillMode = kCAFillModeRemoved
For some reason, at least on the Simulator (I don't think this should matter, as this app should work on older devices, or powerful devices like iPhone 7+), the animation doesn't always land in the same spot.
Sometimes, the cell will fly in and be about 8 pixels from the left, sometimes about 12, sometimes about 20. It doesn't seem consistant. But I think my toValue being 0, it should appear where it would appear normally if I didn't add an animation to the layer.
I tried setting isRemovedOnCompletion to true, and fillMode to removed, but this didn't seem to fix the animation.
On a side note, as the animation continues, there is one point where a cell is made from a horizontal UIStackView, where the arrangedSubviews are hidden in it. I use the UIView.animateWithDuration method to set their hidden values to false, and thus, animate the subviews into existence.
The reason I note this is because when this happens, the cells up above pop back into the positions they should be, instead of being a few pixels off.
I tried setNeedsDisplay on the cells after they animate, and also on the verticalStackView, which didn't work either. I don't get what is going on and why these layers just won't animate into their correct positions. Any ideas anyone?
P.S. The vertical stackView is all initialized in the ViewController's init method. The animations occur in the viewDidAppear method. I tried offsetting the animation by adding DispatchQueue.main.asyncAfter, and also tried it first thing after super.viewDidAppear(animated), but both had the same results.

Animating items into and out of a stack view is actually VERY easy.
All you have to do is to set the frame of the view to the place you want it to start before adding it to the stack view, and then call layoutIfNeeded() inside an animation block immediately after adding the new view. Here's some sample code taken from a working app:
let newView = createNewView()
self.stackView.addArrangedSubview(newView)
if sender != nil {
UIView.animate(withDuration: 0.2) {
self.stackView.layoutIfNeeded()
}
}

Figured out the issue. Here's the problem...
1) You want to animate just the presentation layer, but keep the actual position of the elements due to UIStackView positioning things well for you. So CoreAnimation seems like a great option.
2) Because you're dealing with a UIStackView, at first glance, you can't animate position constraints for its arrangedSubviews. So it's another reason you don't want to animate constraints, and go with CoreAnimation.
So you choose CoreAnimation. Since it's only the presentation layer that matters, no need to animate constraints right? Only really need to do those if you want the user interactive portion of the views to adhere to the constraints right? Well yes, and no.
But for whatever reason, CoreAnimation doesn't do what it's supposed to do. Maybe Apple will fix that in the future. But Maybe not. Oh well, it doesn't matter. Here's the solution:
Use a placeholder for all of your arrangedSubviews in the stackview. For me, this was just a UIView that would resize itself based on the contents within it.
Now, let this placeholder have no background color. Only allow it to have visible content that you don't ever want to animate. Since I'm animating the entire cell, I just have a blank UIView with no backgroundColor.
Set constraints for subviews of this placeholder view with respect to this placeholder view. Now, you can animate these constraints with the usual AutoLayout animation tools. For example:
constraint.constant = 0
UIView.animate(withDuration: 0.2,
delay: 0.1,
options: UIViewAnimationOptions.curveEaseOut,
animations: {
cell.layoutIfNeeded()
}, completion: nil)
Booyah! Animations are smooth and actually end up where you want them relative to the arrangedSubview. Woohoo!

Related

Swift - Animate only specific constraints

I'm trying to animate constraints in my ViewController.
In order to do that, I added this line to my code:
I'm just trying to change the height of a UIView() (from 0 to 100)
barHeight.constant = CGFloat(100)
UIView.animate(withDuration: 2) {self.view.layoutIfNeeded()}
The problem is that, with that line, all of the constraints are animated, and it's not what I would like.
Do you know how I could animate only specific constraints and not others?
Call layoutSubviews before you edit the constraint you want to animate. This will cause any pending layout updates to be applied without any animation and then you can change the next constraint with animation.
Like so:
self.view.layoutSubviews()
barHeight.constant = CGFloat(100)
UIView.animate(withDuration: 2) {self.view.layoutIfNeeded()}

How to animate a UIImage constraints in Swift 4

I am making a card app and I need to make an animation so that a card would change its constraints to move to another place. How would I do this for a UIImage.
Hook the leading , trailing , CenterX OR CenterY constraint of the UIImageView and do this
self.imageLeadCon.constant = // value
UIView.animate(withDuration: 3.0 , animations: {
self.view.layoutIfNeeded()
// animation
}, completion: { _ in
// completion
})
I think you're confusing UIImage with UIImageView. The first one is the image's data representation, the second one is the View that displays the UIImage.
So I guess you want to move around the UIImageView. To do that obtain a reference to the constraints (e.g. by ctrl-dragging from the constraint in the storyboard to your UIViewController instance).
After that you can update the constraint in an animation block like here:
// Make sure the view is laid out as Mischanya Schtrigel stated
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.5) { // duration in seconds
myConstraint.constant = 50 // update your constraint here
self.view.layoutIfNeeded()
}
Contrary to the other answers, I like putting the constraint changes in the animation closure to make it more clear what is going to be animated.
First of all you have to drag and drop your constraint into your view controller, to make an outlet. The same way you are doing it with UIImageView or UITableView, etc.
Actually, for those who have watched WWDC (2014 I guess), Apple have explained how to animate UI in the proper way.
Firstly, you have to call layoutIfNeeded() method to make sure that everything on your view have laid out. After you can change your constraint and right after that in your animation block you call layoutIfNeeded() method to layout your constraint with the new value.
So code should look like that:
view.layoutIfNeeded() // to make sure your view have laid out
self.constraint.constant = //change your constant to any value
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded() //layout your constraint with the new value
}

How to set a DropShadow on UITableView itself

I'm trying to set a dropshadow on a UITableView which is being added programmatically.
The frame height is being set to a certain percentage of the screen. So I want to set a dropshadow on the tableview itself.
I tried doing the following:
tableview_results.layer.shadowPath = UIBezierPath(rect: tableview_results.frame).cgPath
tableview_results.layer.shadowColor = UIColor.black.cgColor
tableview_results.layer.opacity = 1
tableview_results.layer.shadowOffset = CGSize.zero
tableview_results.layer.shadowRadius = 10
However this doesn't seem to do anything.
When I try searching for a solution, I only find stuff regarding how to set a dropshadow on the last cell of the UITableView. However this would not work for me since then the shadow will only be displayed when the last cell is displayed.
I need the shadow to be always present on the UITableView.
The shadow path should be set to the tableview_results.bounds not frame. The bounds is the rectangle around the table view in the coordinate system of the table view itself, and that's the correct coordinate system in which to specify the shadow path. Alternatively, you can just not set the shadow path at all and the shadow will draw in the correct place (though perhaps there's a performance benefit to setting the path explicitly if you can do so reliably).
You also need to set the shadowOpacity not the opacity of the layer, to 1. And you need to tell the tableview_results to not clipToBounds. So something like this:
tableview_results.layer.shadowPath = UIBezierPath(rect: tableview_results.bounds).cgPath
tableview_results.layer.shadowColor = UIColor.black.cgColor
tableview_results.layer.shadowOpacity = 1
tableview_results.layer.shadowOffset = CGSize.zero
tableview_results.layer.shadowRadius = 10
tableview_results.clipsToBounds = NO;
And for that last line, if you feel like the code is cleaner by only talking to the layer, you can equivalently use tableview_results.layer.masksToBounds = NO.
Note that there is a side effect of turning off clipping: Now you might see table view cells beyond the bounds of the table view itself! So there might be better ways to get the shadow effect. You could, for example, wrap the table view in a simple superview that tightly bounds the tableview, and give the shadow to that superview. Just a thought.

Photo extension storyboard with Autolayout error

Recently i am writing an photo extension for my app.
However, i find the extension's storyboard cannot figure out the frame for each view with AutoLayout correctly.
For example I set this in the storyboard:
However when i run the simulator it shows:
Also, when i check the frame for my customized view in didMoveToWindow(), it also shows the incorrect frame :
(66.0, 512.0, 468.0, 30.0)
Actually, the customized slider works fine in my app's storyboard.
What's wrong with me?
Ps: the slider here is a customized UIView not a UISlider
Edit:
By replacing the class to UIView (with color) on storyboard, i am now sure it is not the problem of Autolayout constraints because everything works fine.
Then i found the problem is in the process that I init this customized UIView, I set up my knobs and bar in the method didMoveToWindow(), whose frames depend on the View's frame. When i print the frame of the View at the top of didMoveToWindow(), the frame is not correct at all.
So I cannot get the correct frame of a UIView in its didMoveToWindow() method when using Autolayout?
But that works fine in my original app. So i am confused.
Edit2
I use a delay function to re-check its frame after 5 seconds :
func delay(#seconds: Double, completion:()->()) {
let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64( Double(NSEC_PER_SEC) * seconds ))
dispatch_after(popTime, dispatch_get_main_queue()) {
completion()
}
}
override func didMoveToWindow() {
delay(seconds: 5, {
println(self.frame)
})
}
5 seconds later, the frame is correct! So, when the method is called, constraints are not applied yet. So now what i need to do is to get call-back in UIView when its superView's ViewController finish ViewDidLoad.
Edit3
I find the answer. I should set up subView's frame in UIView's layoutSubviews() method. In that case i can get the right frame when using Autolayout.
Actually, after the UIView's constraints are applied, it will call its layoutSubviews()
Read this => it's helps you alot :)
learn-to-love-auto-layout
OR
if you want to LOVE AUTO LAYOUT… PROGRAMMATICALLY
Rather than giving trailing and leading space, apply this simple constraints.
Make your height constant (If you want to!)
Put UIView in center of the container
Give "Horizontal Center in Container" constraint
Apply appropriate "top space" and "bottom space"
That's it! You're done.

"Magic Move" Effect Custom Transition

I trying to get a custom transition between two View Controllers. First, here's a picture to illustrate what I want :
I want a UICollectionViewCell to expand to the whole screen. In this cell, the subviews are placed with Autolayout in IB.
I just want each subview to go to the new position. So I tried subview.frame = newSubview.frame in the animation block, but it doesn't work (because of Autolayout I think).
I thought to delete the constraints while the animation is occuring, but it doesn't work.
I also tried to make #IBOutlets of the constraints and to change constant property.
Here's my code :
let detailView = detailViewController.view
let cellView = self.selectedCell
container.addSubview(cellView!)
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration, delay: 0.0, options: .CurveEaseInOut, animations: {
let newFrame = detailViewController.view.frame
cellView!.frame = newFrame
cellView!.imageView.frame = newFrame
cellView!.labelTopConstraint.constant = 27
cellView!.labelRightConstraint.constant = 8
cellView!.layoutIfNeeded()
}
...
Actually, when the animation begins the labels snap to a position, then they move and at the end they are not at the right position...
Any ideas ? Thanks
Check out this repo, I think this is what you are looking for BCMagicTransition: https://github.com/boycechang/BCMagicTransition
The issue with your code is that you aren't converting the frames of your subviews of your collection view cell that you want to animate to the coordinates of the container view controller of the transition, and same with the final frames for their positions. Even though you are using AutoLayout you can still manipulate views with their frames.
To accomplish this, checkout this method (documentation):
func convertRect(rect: CGRect, fromView view: UIView?) -> CGRect
You also should look into animating snapshots of the views you wish to animate. Then just add the snapshots to the container view, and animate them to the converted final frames, then remove them when the animation is complete.
Hope this helps !

Resources