UIView animated position jumping - ios

I have a transition animation where I need to make it appear that an item on VC1 moves to a new location as VC2 is swiped into position. I've had no problems making this work on other transitions, but for this one I have an issue I can't figure out.
VC1 - has an image in the center at a particular size
VC2 - same image is placed in the top right corner at a smaller size
So I need to move and scale, since I'm doing this in a transition I thought the right way to do this is:
take image from VC2 translate and scale it to match image on VC1 (using transform)
hide image on VC1
animate image from VC2 back to .identity
I've successfully done this exact same thing when moving something to the top left corner. However when I do this particular animation I'm seeing my VC2 image jump as the animation starts to a new location. I only see this jump when I'm working on a device/simulator that is larger or smaller than a iPhone7, and it jumps differently based on the screen width (left for smaller, right for larger).
I've logged out the position of my image at all stages:
transform from: (x:336.0, y:35.5, w:38.0) to: (x:160.0, y:180.5, w:66.0)
xOffset: -176.0 yOffset: 145.0
transform: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: -176.0, ty: 145.0)
preanimation: (x: 160.0, y: 180.5
currentPosition: (midX: 105.0, midY: 180.5)
currentPosition: (midX: 112.312, midY: 174.476)
First line of this log shows that I'm moving my image from (336, 35) to (160, 180.5). Second line shows that my math is correct. Third line shows the transform that was created (scaling currently turned off). Fourth line shows that my image is in the correct spot just before the animation starts. Last 2 lines are returned in a cadisplaylink callback showing that my position jumps before the animation starts.
Has anyone run into something like this? Any clues how I can solve this? My images are all placed with LayoutConstraints, my animations are all run using transforms (transforming before animation then animating back to .identity).
EDIT:
Some added information, when starting in landscape I noticed that my original frames are completely wrong:
transform from: (x:336.0, y:35.5, w:38.0) to: (x:368.0, y:147.666666666667, w:66.0)
xOffset: 32.0 yOffset: 112.166666666667
transform: CGAffineTransform(a: 2.66666666666667, b: 0.0, c: 0.0, d: 2.66666666666667, tx: 32.0, ty: 112.166666666667)
preanimation: (x: 368.0, y: 147.666666666667
currentPosition: (midX: 729.0, midY: 127.666666666667)
So at this point I'm thinking the issue is that my frames that I'm attempting to sync up to aren't correct before the animation because of the autolayout constraints. And when the animation starts all of the frames are re-calculated by the animation using the layout constraints, then my transforms are applied making the jump appear.
EDIT2:
I think I've got it figured out. The issue is that the view I'm animating in hasn't laid out it's subviews yet so the frames are off. By calling .setNeedsLayout() and .layoutIfNeeded() on my views during the animation setup I get the correct locations to animate between.
For the curious here are the same logs from my first edit with the addition of when the viewlifecycle methods are called:
viewDidLoad <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
viewWillAppear <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
transform from: (x:336.0, y:35.5, w:38.0) to: (x:368.0, y:147.666666666667, w:66.0)
xOffset: 32.0 yOffset: 112.166666666667
transform: CGAffineTransform(a: 2.66666666666667, b: 0.0, c: 0.0, d: 2.66666666666667, tx: 32.0, ty: 112.166666666667)
preanimation: (x: 368.0, y: 147.666666666667
animation Block: (to.x: 368.0, to.y: 147.666666666667 - (from.x: 368.0, from.y: 147.666666666667
viewWillLayoutSubviews <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
viewDidLayoutSubviews <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
currentPosition: (midX: 729.0, midY: 127.666666666667)

Just incase people don't want to read through the wall of text to find the solution the problem was that I was using the positions of views in different view controllers before all views had been laid out. I'd assumed that the entire view lifecycle had processed in my presenting view controller before the animation logic started, I was wrong.
Animation Call Order:
viewDidLoad
viewWillAppear
animateTransition
viewWillLayoutSubviews
viewDidLayoutSubviews
So the fix was to manually call setNeedsLayout() and layoutIfNeeded() as the start of my animateTransition so that the positions of my views were all known and could be used for transformations.

Related

Animation to Remove Sprite

I am currently making a Sprite and I want it to animate before it disappears.
For example: I want it to animate it in the sense that it disappears from the top of the block until the bottom. To put it another way, I want to the size to decrease slowly until there is nothing left. But I want to give it the appearance that it is disappearing rather than scaling to nothing.
let hand = SKSpriteNode(imageNamed: "hand")
hand.size = CGSize(width: size.width/10, height: size.height/30)
hand.position = CGPoint(x: CGFloat(posX-2)*size.width/10+offsetX, y:CGFloat(posY)*size.height/30+offsetY)
addChild(hand)
tl;dr is it possible to make this sort of effect using SpriteKit in Swift.
Ideal Animation: https://ibb.co/sPsffmK
SKAction is an animation that is executed by a node in the scene. Actions are used to change a node in some way (like move its position over time), but you can also use actions to change the scene, like doing a fadeout.
In your case, you can apply multiple animations:
let move_action = SKAction.moveBy(x: -100, y: 0, duration: 1.0)
let scale_action = SKAction.scale(to: 0.0, duration: 1.0)
let fade_action = SKAction.fadeAlpha(to: 0.0, duration: 1.0)
hand.run(move_action)
hand.run(scale_action)
//hand.run(fade_action)
In the previous example, the hand runs the move and scale animation at the same time. But you can also make the hand to move to the position, and after it reaches, to scale it.
let sequence = SKAction.sequence([move_animation,scale_animation])
hand.run(sequence)
There are lots of animations that SKAction has, here you can find the complete list.

Performance optimization of waveform drawing

I'm building an app that draw the waveform of input audio data.
Here is a visual representation of how it looks:
It behaves in similar way to Apple's native VoiceMemos app. But it lacks the performance. Waveform itself is a UIScrollView subclass where I draw instances of CALayer to represent purple 'bars' and add them as a sublayers. At the beginning waveform is empty, when sound input starts I update waveform with this function:
class ScrollingWaveformPlot: UIScrollView {
var offset: CGFloat = 0
var normalColor: UIColor?
var waveforms: [CALayer] = []
var lastBarRect: CGRect?
var kBarWidth: Int = 5
func updateGraph(with value: Float) {
//Create instance
self.lastBarRect = CGRect(x: self.offset,
y: self.frame.height / 2,
width: CGFloat(self.barWidth),
height: -(CGFloat)(value * 2))
let barLayer = CALayer()
barLayer.bounds = self.lastBarRect!
barLayer.position = CGPoint(x: self.offset + CGFloat(self.barWidth) / 2,
y: self.frame.height / 2)
barLayer.backgroundColor = self.normalColor?.cgColor
self.layer.addSublayer(barLayer)
self.waveforms.append(barLayer)
self.offset += 7
}
...
}
When last rect of purple bar reaches the middle of screen I begin to increase contentOffset.x of waveform to keep it running like Apple's VoiceMemos app.
The problem is: when bar count reaches ~500...1000 some noticeable lag of waveform begin to happen during setContentOffset.
self.inputAudioPlot.setContentOffset(CGPoint(x: CGFloat(self.offset) - CGFloat(self.view.frame.width / 2 - 7),y: 0), animated: false)
What can be optimized here? Any help appreciated.
Simply remove the bars that get scrolled off-screen from their superlayer. If you want to get fancy you could put them in a queue to reuse when adding new samples, but this might not be worth the effort.
If you don’t let the user scroll inside that view it might even be worth it to not use a scroll view at all and instead move the visible bars to the left when you add a new one.
Of course if you do need to let the user scroll you have some more work to do. Then you first have to store all values you are displaying somewhere so you can get them back. With this you can override layoutSubviews to add all missing bars during scrolling.
This is basically how UITableView and UICollectionView works, so you might be able to implement this using a collection view and a custom layout. Doing it manually though might be easier and also perform a little better as well.
I suspect that this will probably be a larger refactoring than you can afford but SpriteKit would probably give you better performance and control over rendering of the waveform.
You can use SpiteNodes (which are much faster than shape nodes) for the wave bars. You can implement scrolling by merely moving a parent node that contains all these sprites.
Spritekit is quite good at skipping non visible node when rendering and you can even dynamically remove/add node from the scene if the number of nodes becomes a problem.

CGAffineTransfrom: Translating after a rotation

I am trying to animate a rectangle after I have rotated it with CGAffineTransform. The issue I am having is that the rectangle ends up where it is suppose to be, but the "starting" position for the second transform is not what I expect. This is only an issue because I am animating it. Here is my code below:
UIView.animate(withDuration: 5) {
let shift = 200 * CGFloat(2.0.squareRoot() / 2)
view.transform = view.transform.translatedBy(x: shift, y: 0)
}
view is already defined elsewhere and I have already rotated the view with this:
let rotation = CGAffineTransform(rotationAngle: (45/180)*CGFloat(M_PI))
view.transform = rotation
Before the animation, this is what it looks like: 1
For some reason, the transformation starts above (a little offscreen) and then moves down into position. 2
I would like this to happen instead, where it starts from the original picture, and then shifts in the direction of how its rotated. 3
Note I did try to apply the same shift value to both x and y for the translation but that did not fix my issue.

How to position a SCNNode to cover the whole SCNView?

I am very new to SceneKit and your help will be really appreciated!
I have a 200x200 sized SCNView in my UIView, which is at the centre of super view.
I want to put a SCNCylinder inside, such that the SCNCylinder covers full SCNView. I read that all these views of Scenekit are defined in meters, so how do I form a relationship between the dimensions of my screen and the
SCNCylinder.
I tried:
var coinNode = SCNNode()
let coinGeometry = SCNCylinder(radius: 100, height: 2)
coinNode = SCNNode(geometry: coinGeometry)
coinNode.position = SCNVector3Make(0, 0, 0)
coinScene.rootNode.addChildNode(coinNode)
let rotate90AboutZ = SCNAction.rotateByX(-CGFloat(M_PI_2), y: 0.0, z: CGFloat(M_PI_2), duration: 0.0)
coinNode.runAction(rotate90AboutZ)
ibOutletScene.scene = coinScene
But this leaves a margin between my coinScene and the ibOutletScene. How do I remove this space?
I also tried adding Camera:
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 0, 100)
coinScene.rootNode.addChildNode(cameraNode)
But I see random behaviour with this and the coinNode gets hidden! How should I position my camera? Or is there any other way to remove extra space from my ibOutletScene?
Edit:
This is how it looks if I don't add camera. There is a margin between red scene and green coin. I tried multiple sizes for the coin, but I am unable to remove this margin unless I add a camera. But, If I add camera, I get another problem, mentioned below this screenshot.
If I don't add the camera, The rotation animation on coin works perfectly, but If I add camera,the rotation enlarges the and then becomes small again with the animation. How can I rotate it on its axis, without increasing the size?
I am using following code to rotate the coin:
The same code works fine without camera, but enlarges the coin after adding camera. Checkout the snapshot.
let rotate = SCNAction.rotateByX(0, y: -2 * CGFloat(M_PI_2), z: 0, duration: 2)
coinNode.runAction(rotate)
The random behavior might be caused by the last line in your first code snippet. You're starting an animation, and then adding the scene to the view.
Instead, build your scene, attach it to the view, and then start your animation. Setting a non-zero duration for the action will give you a more pleasing transition.
As for the extra space, it would help us understand if you post a screenshot. But you're going to have to do a bit of trigonometry.
It looks like you have a scene that you want to be blocked by a coin, that then rotates out of the way? Simulate that yourself with real objects. Put your eye down at the edge of your desk. Put a coin out a ways from your eye. How far does that coin have to be in order to block particular objects farther away on your desk?
In SceneKit, you can query the field of view of the SCNCamera. You know the size of your coin and the size of the view. Calculate the distance from the camera needed for the projected diameter of your coin to equal the width of your view. Put the coin there.

Setting UIView.transform to arbitrary translate CGAffineTransform does nothing

I have a UIView called container that I want to move (offset) using affine transfrom. This view contains UIImageView and is a subview of UICollectionViewCell.
So it should be simple:
container.transform = CGAffineTransformMakeTranslation(100, 200) //render container 100 points right and 200 points down
Instead it is very hard, because theat code does not do anything. The view is rendered excatly on the same place as if I delete that line. So I added 'print' to verify what affine translation was set:
container.transform = CGAffineTransformMakeTranslation(100, 200)
print(container.transform) //prints: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 100.0, ty: 200.0)
That seems all right. So I tried rotating the container view instead with CGAffineTransformMakeRotation and it rotates the view just not around its center as it should according to documentation. I tried different combinations of translate, rotation and scale transforms just to find that the affine transformation matrixes set are OK, but attributes tx and ty seems to be ignored and a, b, c and d seems to be using different anchor point then the centre of the view (cannot say what that point is).
Any ideas on what can be causing this and how to fix it?
There must be something like auto layout messing things up for you. In the absence of outside influence, setting a view's affine transform to CGAffineTransformMakeTranslation(100, 200) will shift it right 100 points and down 200. I verified this by making a new Single View Project in Xcode and changing the viewDidLoad method in the ViewController.swift class to:
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = UIColor.blueColor();
let container = UIView(frame: CGRectMake(0,0,100,100));
container.backgroundColor = UIColor.greenColor();
container.transform = CGAffineTransformMakeTranslation(100, 200);
view.addSubview(container);
}
As expected this makes the green container view appear 100 points to the right and 200 points down, even though its frame is (0,0,100,100).
So please check for auto layout and other such things that might influence the placement of this view, and if you can't find anything please post more code. Also, if your container view doesn't have a background color, please give it one so that you can see its position directly, instead of deducing its position by looking at the image view.
n.b. Setting a view's transform doesn't actually move the view itself, it just changes how/where it draws its content.

Resources