The middle lock animates upwards faster than the top pane in my animation. How do I animate the middle lock so that it moves upwards alongside the top pane? To be more specific, I want the middle locks center to always align with the top panes bottom as it moves up and off the screen.
This is the code I currently have, lockBorder and lockKeyhole is what is referred to as "middle lock" which is moving upwards too fast compared to the topLock:
#IBOutlet var topLock: UIImageView!
#IBOutlet var bottomLock: UIImageView!
#IBOutlet var lockBorder: UIImageView!
#IBOutlet var lockKeyhole: UIImageView!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
openLock()
}
func openLock() {
UIView.animateWithDuration(0.5, delay: 1.0, options: [], animations: {
self.lockKeyhole.transform = CGAffineTransformMakeRotation(CGFloat(3.14))
}, completion: { _ in
UIView.animateWithDuration(0.6, delay: 0.2, options: [], animations: {
var topFrame = self.topLock.frame
topFrame.origin.y -= topFrame.size.height
var bottomFrame = self.bottomLock.frame
bottomFrame.origin.y += bottomFrame.size.height
var lockBorderFrame = self.lockBorder.frame
lockBorderFrame.origin.y -= self.view.frame.height
var lockKeyholeFrame = self.lockKeyhole.frame
lockKeyholeFrame.origin.y -= self.view.frame.height
self.topLock.frame = topFrame
self.bottomLock.frame = bottomFrame
self.lockKeyhole.frame = lockKeyholeFrame
self.lockBorder.frame = lockBorderFrame
}, completion: { finished in
})
})
}
You are adjusting topFrame.origin.y by topFrame.size.height, but you are adjusting lockBorderFrame.origin.y and lockKeyholeFrame.origin.y by self.view.frame.height. We can deduce that these adjustments are different amounts. I assume topFrame.size.height is one half of self.view.size.height. Since you're moving the lock views by twice the distance you're moving topLock, and both adjustments happen over the same 0.6 second interval, the lock views move twice as fast.
I assume you want to animate the lock views all the way off the screen, so it would be insufficient to move them by by only topFrame.size.height. That would leave the bottom half of the lock visible. You need to move the lock views so that their bottom edges are at the top edge of self.view.
This means that you need to set lockBorder.frame.origin.y to the negative of lockBorder.frame.size.height. It will move a total distance of lockBorder.frame.size.height + lockBorder.frame.origin.y, which happens to be equal to lockBorder.frame.maxY.
I assume that lockBorder is at least as large as lockKeyhole, so if we move all the views by lockBorder.frame.maxY, they will all be off the screen. And since we move them all the same distance, all the views will move together.
Note also that if you use constraints in your storyboard, then these views will snap back to their original positions as soon as you do almost anything else to the view hierarchy. So you should probably remove them from their superviews as soon as the animation finishes, unless you're hiding them in some other way (like by removing their superview from the view hierarchy).
Incidentally, the standard library defines M_PI for you.
func openLock() {
UIView.animateWithDuration(0.5, delay: 1.0, options: [], animations: {
self.lockKeyhole.transform = CGAffineTransformMakeRotation(CGFloat(M_PI))
}, completion: { _ in
UIView.animateWithDuration(0.6, delay: 0.2, options: [], animations: {
let yDelta = self.lockBorder.frame.maxY
self.topLock.center.y -= yDelta
self.lockKeyhole.center.y -= yDelta
self.lockBorder.center.y -= yDelta
self.bottomLock.center.y += yDelta
}, completion: { _ in
self.topLock.removeFromSuperview()
self.lockKeyhole.removeFromSuperview()
self.lockBorder.removeFromSuperview()
self.bottomLock.removeFromSuperview()
})
})
}
Related
I have an application that animates a small, red subview using added animations. The first animation moves the view down by 100 points. The second animation kicks in at the half way point and moves the view to the right by 100 points. The down and the right animations are additive resulting in a diagonal movement toward the lower right. Additionally, both animations are autoreversed so that the view is restored to its original position over the top of the small, green "marker" subview.
From the above gif one can see that the view animation behaves as expected until the autoreversing has completed whereupon the view jumps to the left (apparently by 100 points). Typically such a jump would be the result of not having set the state of the view to match the final frame of the animation. However, the view state is indeed being correctly(?) set by the animator's completion method - this seems to be borne out by the information provided by the print statements.
What is the cause of the view's final, leftward jump?
See the relevant code below. A complete project can be downloaded from GitHub
I have a hunch that the calls to UIView.setAnimationRepeatAutoreverses in each of the two animation closures might be the culprit. It is not clear to me that what I have done in that regard is in alignment with the framework's expectations.
class ViewController: UIViewController {
private var markerView: UIView!
private var animationView: UIView!
override func viewDidLoad() {
view.backgroundColor = .black
view.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(tapGestureHandler)))
let frame = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 10, height: 10)
markerView = UIView(frame: frame)
markerView.backgroundColor = .green
view.addSubview(markerView)
animationView = UIView(frame: frame)
animationView.backgroundColor = .red
view.addSubview(animationView)
}
#objc private func tapGestureHandler(_ sender: UITapGestureRecognizer) {
animate()
}
// Move down; half way through begin moving right while still moving down.
// Autoreverse it.
private func animate() {
let distance: CGFloat = 100
let down = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.y += distance
}
let right = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.x += distance
}
print("\nCenter: \(self.animationView.center)")
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
self.animationView.center.x -= distance
self.animationView.center.y -= distance
print("Center: \(self.animationView.center)")
}
animator.startAnimation()
}
}
Update 1
I have added two other versions of the animate method which are informative. Based upon what I learned from these two versions, I altered the title of this question.
animate2: The calls to UIView.setAnimationRepeatAutoreverses have been commented out in each of the down and right animation functions. This modified code functions as expected. Of course we do not get the smooth autoreversing effect.
extension ViewController {
private func animate2() {
let distance: CGFloat = 100
let down = {
//UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.y += distance
}
let right = {
//UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.x += distance
}
print("\nCenter: \(self.animationView.center)")
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
self.animationView.center.x -= distance
self.animationView.center.y -= distance
print("Center: \(self.animationView.center)")
}
animator.startAnimation()
}
}
animate3: The addition of the second (i.e. right) animation is commented out. This modified code functions as expected. Of course we do not get the movement to the right.
extension ViewController {
private func animate3() {
let distance: CGFloat = 100
let down = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.y += distance
}
let right = {
UIView.setAnimationRepeatAutoreverses(true)
self.animationView.center.x += distance
}
print("\nCenter: \(self.animationView.center)")
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
//animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
self.animationView.center.y -= distance
//self.animationView.center.x -= distance
print("Center: \(self.animationView.center)")
}
animator.startAnimation()
}
}
It appears that calling UIView.setAnimationRepeatAutoreverses in both of the animation functions is "messing things up". It is not clear to me if this can be rightly be regarded as an iOS bug.
Just comment the line under your animate() method ::
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
animator.addAnimations(right, delayFactor: 0.5)
animator.addCompletion { _ in
print("Center: \(self.animationView.center)")
//self.animationView.center.x -= distance //<--- Comment or remove this line.
self.animationView.center.y -= distance
print("Center: \(self.animationView.center)")
}
Xcode swift 4
I have some views that are added dynamically in code one below another.
Each new view top anchor is connected to previous view bottom anchor.
And each view have a button that make view to expand/collapse with animation. Here is button code :
let fullHeight : CGFloat = 240
let smallHeight : CGFloat = 44
let currentHeigth = rootView.frame.size.height //I use this to get current height and understand expanded view or not
let heighCons = rootView.constraints.filter //I use this to deactivate current height Anchor constraint
{
$0.firstAttribute == NSLayoutAttribute.height
}
NSLayoutConstraint.deactivate(heighCons)
rootView.layoutIfNeeded()
if currentHeigth == smallHeight
{
rootView.heightAnchor.constraint(equalToConstant: fullHeight).isActive = true
rootView.setNeedsLayout()
UIView.animate(withDuration: 0.5)
{
rootView.layoutIfNeeded() //animation itself
}
}
else
{
rootView.heightAnchor.constraint(equalToConstant: smallHeight).isActive = true
rootView.setNeedsLayout()
UIView.animate(withDuration: 0.5)
{
rootView.layoutIfNeeded() //animation itself
}
}
This all works perfectly but i have a problem : view that below current expanding view changes it y position immediately with no animation. Its just jumping to previous view bottom anchor, that would be active after animation finish.
So my question is :
1) what is the right way to make height constraint animation, when views are connected to each other by bottom/top animation?
2) my goal is just to make a view that would expand/collapse on button click, maybe i should do it another way?
Here's an approach using Visiblity Gone Extension
extension UIView {
func visiblity(gone: Bool, dimension: CGFloat = 0.0, attribute: NSLayoutAttribute = .height) -> Void {
if let constraint = (self.constraints.filter{$0.firstAttribute == attribute}.first) {
constraint.constant = gone ? 0.0 : dimension
self.layoutIfNeeded()
self.isHidden = gone
}
}
}
Usage
expanview.visiblity(gone: true,dimension: 0)
Example
#IBOutlet weak var msgLabel: UILabel!
#IBOutlet weak var expanview: UIView!
#IBAction func toggleCollapisbleView(_ sender: UIButton) {
if sender.isSelected{
sender.isSelected = false
expanview.visiblity(gone: false,dimension: 128)
sender.setTitle("Collapse",for: .normal)
}
else{
sender.isSelected = true
expanview.visiblity(gone: true,dimension: 0)
sender.setTitle("Expand",for: .normal)
msgLabel.text = "Visiblity gone"
}
}
Modus Operandi:
1) Use an UIImageView of a base Clock Image.
2) Add MinuteHand & HourHand sublayers (containing their respective images) to the UIImageView layer.
Problem: both sublayers disappear when attempting to perform a rotation transformation.
Note: 1) I've removed the 'hour' code & ancillary radian calculations to simplify code.
2) The 'center' is the center of the clock. I had adjusted the coordinates to actually pin the hands to the clock's center.
3) The ViewDidLayoutSubviews() appear to be okay. I got the clock + hands.
class ClockViewController:UIViewController {
private let minuteLayer = CALayer()
#IBOutlet weak var clockBaseImageView: UIImageView!
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLayoutSubviews() {
guard var minuteSize = UIImage(named: "MinuteHand")?.size,
var hourSize = UIImage(named: "HourHand")?.size
else {
return
}
var contentLayer:CALayer {
return self.view.layer
}
var center = clockBaseImageView.center
// Minute Hand:
minuteLayer.setValue("*** Minute Hand ***", forKey: "id")
minuteSize = CGSize(width: minuteSize.width/3, height: minuteSize.height/3)
minuteLayer.contents = UIImage(named: "MinuteHand")?.cgImage
center = CGPoint(x: 107.0, y: 40.0)
var handFrame = CGRect(origin: center, size: minuteSize)
minuteLayer.frame = handFrame
minuteLayer.contentsScale = clockBaseImageView.layer.contentsScale
minuteLayer.anchorPoint = center
clockBaseImageView.layer.addSublayer(minuteLayer)
}
Here's my problem: Attempting to rotate the minute hand via 0.01 radians:
func set(_ time:Date) {
minuteLayer.setAffineTransform(CGAffineTransform(rotationAngle: .01)) // random value for test.
}
Before rotation attempt:
After attempting to rotate minute hand:
The hand shifted laterally to the right vs rotate.
Why? Perhaps due to the pivot point?
I think this will solve your problem, Take a look and let me know.
import GLKit // Importing GLKit Framework
func set(_ time:Date) {
minuteLayer.transform = CGAffineTransformMakeRotation(CGFloat(GLKMathDegreesToRadians(0.01)))
}
Note: this solution doesn't solve the issue about rotating a CALayer. Instead, it bypasses the issue by replacing the layer with a subview and rotating the subview via:
func set(_ time:Date) {
minuteView.transform = CGAffineTransform(rotationAngle: 45 * CGFloat(M_PI)/180.0)
}
Here's the result:
Still, it would be nice to know how to rotate a CALayer.
I tried adding var square, and all the things I did for squareView. But all I am getting is one box falling and the other just standing there. Is there an easier way to add another box which does the same action as squareView?
import UIKit
class interestViewController: UIViewController {
var squareView: UIView!
var square: UIView!
var gravity: UIGravityBehavior!
var animator: UIDynamicAnimator!
var collision: UICollisionBehavior!
override func viewDidLoad() {
super.viewDidLoad()
squareView = UIView(frame: CGRectMake(100, 100, 100, 100))
square = UIView(frame: CGRectMake(200, 100, 100, 100))
squareView.backgroundColor = UIColor.blackColor()
square.backgroundColor = UIColor.blackColor()
view.addSubview(square)
view.addSubview(squareView)
animator = UIDynamicAnimator(referenceView: view)
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [squareView])
gravity = UIGravityBehavior(items: [square])
animator.addBehavior(gravity)
animator.addBehavior(gravity)
collision = UICollisionBehavior(items: [squareView])
collision = UICollisionBehavior(items: [square])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
This code is sheer nonsense:
gravity = UIGravityBehavior(items: [squareView])
gravity = UIGravityBehavior(items: [square])
animator.addBehavior(gravity)
animator.addBehavior(gravity)
When you set the variable gravity to one behavior and to another behavior right in a row, the first behavior is just thrown out as if it never existed. So naturally when you get around to adding the behaviors, there is only one behavior to add: the one gravity that is left (which you then, even more nonsensically, add twice). So naturally only one block is animated.
Think of it more simply this way:
var x = 0
x = 1
x = 2
Now, what is the value of the variable x? If you think it is 0 and 1 and 2, you have not understood what a variable is.
For some reason after my first call of a serious of animation blocks, the animation seems to be faster, not sure if this is a bug or something i've done wrong but i'm sure someone call tell me.. i've made a UIView subclass to handle this.
import UIKit
import QuartzCore
class GBPopupController: UIView {
var originalContainerCenterY = CGFloat()
#IBOutlet var continerConstraintCenterY: NSLayoutConstraint!
#IBOutlet var containerConstraintCenterX: NSLayoutConstraint!
var startingCenter = CGPoint()
#IBOutlet var contentView: UIView!
#IBOutlet var button: UIButton!
override func layoutSubviews() {
super.layoutSubviews()
contentView.layer.cornerRadius = 10
button.layer.cornerRadius = 10
}
override func didMoveToSuperview() {
self.beginViewAnimations()
}
func animatePopupIn() {
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.7, options: UIViewAnimationOptions.CurveEaseIn, animations: {
self.contentView.layer.transform = CATransform3DIdentity
}, completion: {finished in
})
}
func beginViewAnimations() {
var transform = CATransform3DIdentity;
transform = CATransform3DMakeTranslation(0, -self.frame.size.height, 0)
self.contentView.layer.transform = transform
UIView.animateWithDuration(0.4, delay: 0.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
self.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
}, completion: {finished in
self.animatePopupIn()
})
}
func removeViewFromSuperView() {
UIView.animateWithDuration(0.5, delay: 0.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
var transform = CATransform3DIdentity;
transform = CATransform3DMakeTranslation(0, -self.frame.size.height, 0)
self.contentView.layer.transform = transform
}, completion: {finished in
UIView.animateWithDuration(0.4, delay: 0.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
self.backgroundColor = UIColor.clearColor()
}, completion: {finished in
self.removeViewAnimation()
})
})
}
func removeViewAnimation() {
self.removeFromSuperview()
}
My comment is too long so I answer here. I don't know if it can help you but here's what I did.
I use autolayout and so in the viewdidload(), dimensions are not yet initialized. They are based on a 600x600 size screen (meaning any width, any height). Screen size is picked up once the first action is done (like pushing a button in my case).
This is why I call my animation for the first time at this first action, just to put my frame at the good place. Then the rest is the same. But it kind of initialize my view with the right frame (size and position). After that, I don't have this problem and my animation is also good.
It's like I was calling it twice the first time, one to initialize and one just to play the animation.
I made some research and I found that it could be a problem with the layer which is different from the frame and which is initialized with a different animation from the one you choose. That's why I didn't have the same animation the first time.
Hope it will help you.