iOS animation malfunctioning, Y-Coordinate Problems - ios

For hours I've been trying to solve the smallest animation glitch. My code successfully moves the view off screen, then animates it back in. It gets the x-coordinations right but the Y axis has behavior I don't understand. Here's the code:
func listTrans_slideIn (slideFrom: String) {
//var newFrame = tableView.frame
tableView.frame.origin.x = 1000
//tableView.frame.origin.y = 100
print("Table pushed to side")
UIView.animateKeyframesWithDuration(1.375 /*Total*/, delay: 0.0, options: UIViewKeyframeAnimationOptions.CalculationModeLinear, animations: {
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1/1, animations:{
self.tableView_toTLG_Top.constant = 130
self.tableView_toSV_Left.constant = 0
self.tableView_toSV_Right.constant = 0
self.setupView()
//newFrame.origin.y = self.hdrBox.frame.height+50
//newFrame.origin.x = 0
//self.tableView.frame = newFrame
self.tableView.layoutIfNeeded()
})
},
completion: { finished in
if (!finished) { return }
})
}
The weird behavior is that if I put the correct y-coordinate in the animation keyframe, it comes in too high but then settles at the correct coordinate. If I put in a y-Coordinate that is too low, it comes in at the correct height but then settles too low.
As you can see, I've tried using frames and constraints. I've tried changing the height that I move it off screen to, but that seems to have no effect.
Anyone have any idea why I've spent half my day seeing this bug?

Can you try something like this :
// Use the same value here and below, it moves it out of the screen
self.tableView.center.y -= 500
UIView.animateWithDuration(1.0, animations: { () -> Void in
// Then it comes back
self.tableView.center.y += 500
})

This turned out to be an order of operations problem. The solution required that I move the animation to take place in the viewDidAppear.
Other notes:
To make the animation look as smooth as possible, consider turning off the segue's animation.
Also make sure you're running the segue call and animation in your main thread so that everything happens smoothly and without delay

Related

Why are my UI elements not resetting correctly after being animated/scaled off screen?

So I'll give as much information about this project as I can right up front. Here is an image of a section of the storyboard that is relevant to the issue:
And here is the flow of the code:
1) A user plays the game. This scrambles up the emoji that are displayed and will eventually hide all of the emoji on the right side.
2) When someone wins the game, it calls
performSegue(withIdentifier: "ShowWinScreenSegue", sender: self)
Which will perform the segue the red arrow is pointing to. This segue is a modal segue, over current content, cross dissolve.
3) Stuff goes on here, and then I try to get back to the game screen so the user can play another game. Here is my current code for that
// self.delegate is the GameController that called the segue
// it's set somewhere else in the code so I can call these reset functions
GameController.gs = GameState()
guard let d = self.delegate else {
return
}
d.resetGameToMatchState()
dismiss(animated: true, completion: {
print("Modal dismiss completed")
GameController.gs = GameState()
self.delegate?.resetGameToMatchState()
})
So here's where the issue is. You can see that I have to call delegate?.resetGameToMatchState() twice for anything to actually happen. If I remove the top one, nothing happens when I call the second one and vice-versa. What makes this so annoying is that the user will see a weird jump where all the ui goes from the old state to the new state because it's updating so late and spastically.
What I've tried so far
So this whole issue has made me really confused on how the UI system works.
My first thought was that maybe the function is trying to update the UI in a thread that's executing too early for the UI thread. So I put the whole body of resetGameToMatchState in a DispatchQueue.main.async call. This didn't do anything.
Then I thought that it was working before because when the WinScreenSegue was being dismissed before (when it was a "show" segue) it was calling GameController's ViewDidAppear. I tried manually calling this function in the dismiss callback, but that didn't work either and feels really hacky.
And now I'm stuck :( Any help would be totally appreciated. Even if it's just a little info that can clear up how the UI system works.
Here is my resetGameToMatchState():
//reset all emoji labels
func resetGameToMatchState() {
DispatchQueue.main.async {
let tier = GameController.gs.tier
var i = 0
for emoji in self.currentEmojiLabels! {
emoji.frame = self.currentEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
i=0
for emoji in self.goalEmojiLabels! {
emoji.frame = self.goalEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
//match state
for i in 1...4 {
if GameController.gs.currentEmojis[i] == GameController.gs.goalEmojis[i] {
self.currentEmojiLabels?.findByTag(tag: i)?.isHidden = true
}
}
//reset highlight
let f = self.highlightBarInitFrame
let currentLabel = self.goalEmojiLabels?.findByTag(tag: tier)
let newSize = CGRect(x: f.origin.x, y: (currentLabel?.frame.origin.y)!, width: f.width, height: (currentLabel?.frame.height)! )
self.highlightBarImageView.frame = newSize
//update taps
self.updateTapUI()
//update goal and current emojis to show what the current goal/current selected emoji is
self.updateGoalEmojiLabels()
self.updateCurrentEmojiLabels()
}
}
UPDATE
So I just found this out. The only thing that isn't working when I try to reset the UI is resetting the right side emoji to their original positions. What I do is at the start of the app (in viewDidLoad) I run this:
for emoji in currentEmojiLabels! {
currentEmojiLabelInitFrames.append(emoji.frame)
}
This saves their original positions to be used later. I do this because I animate them to the side of the screen before hiding them.
Now when I want to reset their positions, I do this:
var i = 0
for emoji in self.currentEmojiLabels! {
emoji.frame = self.currentEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
this should set them all to their original frame and scale, but it doesn't set the position correctly. It DOES reset the scale though. What's weird is that I can see a tiny bit of one of the emoji off to the left of the screen and when they animate, they animate from far off on the left. I'm trying to think of why the frames are so off...
UPDATE 2
So I tried changing the frame reset code to this:
emoji.frame = CGRect(x: 25, y: 25, width: 25, height: 25)
Which I thought should reset them correctly to the top left, but it STILL shoves them off to the left. This should prove that the currentEmojiLabelInitFrames are not the issue and that it has something to do with when I'm setting them. Maybe the constraints are getting reset or messed up?
Your first screen, GameController, should receive a viewWillAppear callback from UIKit when the modal WinScreenController is being dismissed.
So its resetGameToMatchState function could simply set a property to true, then your existing resetGameToMatchState could move into viewWillAppear, checking first if the property is being set.
var resetNeeded: Bool = false
func resetGameToMatchState() {
resetNeeded = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// reset code here
}
TLDR; Reset an element's scale BEFORE resetting it's frame or else it will scale/position incorrectly
Finally figured this out. Here's a bit more background. When an emoji is animated off the screen, this is called:
UIView.animate(withDuration: 1.5, animations: {
let newFrame = self.profileButton.frame
prevLabel?.frame = newFrame
prevLabel?.transform = CGAffineTransform(scaleX: 0.1, y: 0.1);
}) { (finished) in
prevLabel?.isHidden = true
}
So this sets the frame to the top left of the screen and then scales it down. What I didn't realize is that when I want to reset the element, I NEED to set the scale back to normal before setting the frame. Here is the new reset code:
for emoji in self.currentEmojiLabels! {
emoji.transform = CGAffineTransform(scaleX: 1, y: 1) //this needs to be first
emoji.frame = self.currentEmojiLabelInitFrames[i] //this needs to be after the scale
emoji.isHidden = false
i+=1
}

UIStackView animation

I am trying to animate playing cards between UIStackViews and have spent countless hours trying to get it right. I feel this should be pretty straight forward, but I haven't found any information to help me.
Clip here: https://pixlig.se/animate.mov
The idea here is to make things look as natural as possible, i.e. the cards picked should animate from their origin to (the bottom of) the stock cards.
The code that produces the result in the clip is the following.
func collectCards(_ cards: Array<Card>, player: Player) {
var delay = 0.5
var cardPoints = 0
// Loop through collected cards, insert them in stock and remove them from table hand
for card in cards {
UIView.animate(withDuration: 0.3, delay: delay, options: [], animations: {
card.frame.origin = card.origin
}, completion: { (finished: Bool) -> Void in
//player.stock.insert(card, at: 0)
})
player.stock.insert(card, at: 0)
tableCards.hand.remove(at: tableCards.hand.index(of: card)!)
cardPoints += card.getCardPoints(card)
delay += 0.1
}
}
Now, card.origin get's its CGPoint when the card is tapped like so:
card.origin = view.convert(card.frame.origin, to: player1.stockView)
and that's where I'm doing it wrong I guess. All I want is the distance from the tapped card to the end position. So am I going about this the wrong way?
If you want to take a closer look at the whole project you can find it here: https://github.com/matsmorot/Smulle

Updating UILabels Off-Screen Causing Them To "Snap" Back

Background Info
I am building a tiny stopwatch app. It consists of a main view with a label displaying the time and two buttons: start & stop.
The start button activates NSTimers which then call methods that calculate the time passed and then update the labels accordingly. The stop button simply invalidates the timers.
On the upper right hand corner, there is an arrow button that is supposed to move all the UI elements to the left and show a menu. That works great when the timers and not running and thereby the labels are not being updated.
Issue
However, when running the timers & thereby updating the labels simultaneously, shortly after beginning the animation, everything snaps back to the position it was in previously. When invalidating the timers with the stop button, the animation is working fine again.
Demo
Code
Animation in toggleMenu()
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5, options: .CurveEaseOut, animations: {
if self.arrow.frame.origin.x >= screenBounds.width/2 {
//if menu is NOT visible (mainView ON-SCREEN, menu right)
for element in self.UIElements {element.frame.origin.x -= screenBounds.width}
self.arrow.frame.origin.x = 0
self.arrow.transform = CGAffineTransformMakeRotation(CGFloat(-M_PI))
} else {
//if menu IS visible (mainView left, menu ON-SCREEN)
for element in self.UIElements {element.frame.origin.x += screenBounds.width}
self.arrow.frame.origin.x = screenBounds.width-self.arrow.frame.width
self.arrow.transform = CGAffineTransformMakeRotation(CGFloat(0))
}
}, completion: nil)
Updating Labels in timer-called updateTime()
let currentTime = NSDate.timeIntervalSinceReferenceDate()
var elapsedTime: NSTimeInterval = currentTime - startTime
let hrs = UInt8(elapsedTime/3600)
elapsedTime -= NSTimeInterval(hrs)*3600
let mins = UInt8(elapsedTime/60)
elapsedTime -= NSTimeInterval(mins)*60
let secs = UInt8(elapsedTime)
elapsedTime -= NSTimeInterval(secs)
let strHrs = String(format: "%02d", hrs)
let strMins = String(format: "%02d", mins)
let strSecs = String(format: "%02d", secs)
//update time labels
self.time.text = "\(strHrs):\(strMins)"
self.secs.text = "\(strSecs)"
Attempts
With no luck, I tried...
...hiding the labels once the animation is finished
...animating / hiding all the elements' superview once the animation is finished (clipsToBounds set true)
Note
I figured...
...commenting out the lines where the labels are being updated "fixes" the issue
...animating the UI elements to move only a couple pixels (like 100px) causes the same problem, meaning that the issue happens on- and off-screen
Any thoughts?
Thanks in advance!
When you update the labels, Auto Layout is running and placing your UI elements back to where their constraints say they should be. You shouldn't alter the frames of objects that are under the control of Auto Layout or you will get unexpected results.
Instead of updating the origin.x of your UI elements, create #IBOutlets to the NSLayoutContraints that position them horizontally and then update the constant property of the constraints and call self.view.layoutIfNeeded() in your animation loop.
It might be easier to make all of your UI elements be a subview of a top level UIView, and then you'd only need to update the one constraint that places that UIView horizontally to move it offscreen.

dismiss view controller custom animation

I am trying to replicate this animation to dismiss a view controller (15 second video): https://www.youtube.com/watch?v=u87thAbT0CQ
This is what my animation looks like so far: https://www.youtube.com/watch?v=A2XmXTVxLdw
This is my code for the pan gesture recognizer:
#IBAction func recognizerDragged(sender: AnyObject) {
let displacement = recognizer.translationInView(view)
view.center = CGPointMake(view.center.x + displacement.x, view.center.y + displacement.y)
recognizer.setTranslation(CGPoint.zero, inView: view)
switch recognizer.state {
case .Ended:
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layer.position = CGPoint(x: self.view.frame.width / 2, y: self.view.frame.height / 2)
})
default:
print("default")
}
let velocity = recognizer.velocityInView(self.titleView)
print(velocity)
if velocity.y < -1500 {
up = true
self.dismissViewControllerAnimated(true, completion: nil)
}
if velocity.x > 1500 {
right = true
self.dismissViewControllerAnimated(true, completion: nil)
}
}
It may be a little hard to notice in my video, but there is a small disconnect in how fast the user flicks up, and how fast the animation completes. That is to say, the user may flip up very fast but the animation is set to a hardcoded 0.3 seconds. So if the user flicks the view fast, then as the animation completes, as soon as their finger lifts off the view, the animation actually slows down.
I think what I need is a way to take the velocity recorded in the recognizerDragged IBAction, and pass that to the animation controller, and based on that, calculate how long the animation should take, so that the velocity is consistent throughout, and it looks smooth. How can I do that?
Additioanlly, I'm slightly confused because the Apple Documentation says that the velocityInView function returns a velocity in points, not pixels. Yet different iOS devices have different points per pixels, so that would further complicate how I would translate the velocity before passing it to the animation class.
Any idea how to pass the velocity back to the animation controller, so that the animation duration changes based on that, and make it work for different iPhones ?
thanks
What you are likely looking at in the video you are trying to replicate is a UIDynamics style interaction, not a CoreAnimation animation. The velocity returned from velocityInView can be used directly in UIDynamics like this:
[self.behavior addLinearVelocity:
[pan velocityInView:pan.view.superview] forItem:pan.view];
I wrote a tutorial for doing this style of view interaction here: https://www.smashingmagazine.com/2014/03/ios7-new-dynamic-app-interactions/
To stick with UIView animations you just need to look at the frame's bottom (which is also in points) and calculate the new time. This assume that you want frame's bottom to be at 0 at the end of the animation:
animationTime = CGRectGetMaxY(frame) / velocity
You aren't showing how you created the animation controller, but just keep a reference to it and pass the time before calling dismiss. This is also assuming you are using a linear curve. With any other kind of curve, you will have to estimate what the starting velocity would have to be to be based on time and adjust.

Forcing a UIView to redraw its contents inside of an animation block

I have a superview A and a subview B. In A's drawRect(_:) I do some line drawing that depends on the position and size of subview B. In class A (but not in its drawRect(_:)) I also have an animation block that moves and scales B.
The problem is that the drawing in drawRect(_:) always happens before the animation takes place, either using the final position and size of the subview or using its initial position and size, but never using the intermediate values.
Case 1: drawing uses the final state
The image below is a screen shot of the simulator while the animation that scales the subview is in progress. Note how the lines are already drawn in their final state.
The code for the line drawing is in A's drawRect(_:). This is the code for the animation (it's in class A so self refers to the superview):
UIView.animateWithDuration(3.0, delay: 0.5,
options: UIViewAnimationOptions.CurveLinear,
animations: {
[unowned self] in
self.subviewWidthConstraint.constant = 0.75 * CGRectGetWidth(self.bounds)
self.layoutIfNeeded()
self.setNeedsDisplay() // this has no effect since self's bounds don't change
}) { (completed) -> Void in }
Case 2: drawing uses the initial state
Looking around for answers I found a suggestion to use self.layer.displayIfNeeded() in place of self.setNeedsDisplay() in the animation block, so I tried this as well
func animateFrame()
{
UIView.animateWithDuration(3.0, delay: 0.5,
options: UIViewAnimationOptions.CurveLinear,
animations: {
[unowned self] in
self.subviewWidthConstraint.constant = 0.75 * CGRectGetWidth(self.bounds)
self.layoutIfNeeded()
self.layer.displayIfNeeded() // the only difference is here
}) { (completed) -> Void in }
}
but it results in the following. Again, this is a screen shot while the animation that scales up the subview is in progress. Now, the lines are again drawn before the animation starts but using the subview's initial position and size.
I think I understand case 1. The entire animation block is queued for execution but its end result is already computed by the time the animation starts so it's no surprise that drawInRect(_:) is using the final state.
I don't understand case 2 but that's because I have little experience with dealing directly with the view layer.
Either way, the effect I'm looking to achieve is that the lines are drawn to the vertices of the subview while it's being moved and/or scaled. Any suggestions or pointers to documentation are much appreciated. Thanks!

Resources