The below function is just moving the view to a new place. It isn't showing the animation:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.9, animations: {
//self.leadingConst.constant -= 200
self.view.setNeedsLayout()
})
}
self.leadingConst.constant -= 200 should be outside UIView.animate, like this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.leadingConst.constant -= 200
UIView.animate(withDuration: 0.9, animations: {
self.view.setNeedsLayout()
})
}
reference: trying to animate a constraint in swift
A few weeks ago I ran into the same problem. The problems came down to a few things. I was mixing adding views programmatically and adding them to the storyboard, I wasn't calling layoutSubviews(), and I was adding my constraints as IBOutlets as weak vars. So I would suggest to only use storyboard (otherwise animations start to get complicated quickly) and make your leadingConst constraint a var. You should also move the object that you want to animate in the animation block and then use a completion block to reset the constraint.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
view.layoutSubviews()
UIView.animate(withDuration: 0.9, animations: {
//something like this
self.animatableObject.origin.x -= 200
}, completion: {(finished: Bool) in
self.leadingConst.constant -= 200
})
}
I'm not sure what your trying to animate so the origin.x line is just an example.
2023 answer
The correct solution was given in a comment, you need to call layoutIfNeeded()
A typical example
UIView.animate(withDuration: t) { [weak self] in
guard let self = self else { return }
self.bottom.constant = -H
self.superview!.layoutIfNeeded()
}
IMPORTANT: "IN OR OUT" IS THE SAME
somewhat bizarrely, it makes NO difference if you do:
this
UIView.animate(withDuration: t) {
self.bottom.constant = -66
self.superview!.layoutIfNeeded()
}
or this
self.bottom.constant = -66
UIView.animate(withDuration: t) {
self.superview!.layoutIfNeeded()
}
There are 1000 totally incorrect comments on SO that you have to do it one way or the other to "mak eit work". It has nothing to do with the issue.
WHY THE SOLUTION IS layoutIfNeeded
It was perfectly explained in the #GetSwifty comment.
layoutIfNeeded is when the actual animation takes place. setNeedsLayout just tells the view it needs to layout, but doesn't actually do it.
WHY THE ISSUE IS CONFUSING: IT "SOMETIMES" WORKS ANYWAY
Often you actually do not need to explicitly have layoutIfNeeded. This causes lots of confusion. ie, this will work "sometimes" ...
UIView.animate(withDuration: t) {
self.bottom.constant = -66
}
There are particular things to consider:
If you are animating "the view itself" (ie likely in a custom UIView subclass), the cycle can be different from when you are animating some view below you "from above"
If you have other animations going on, that can affect the view cycle.
Note that if it "magically works" without layoutIfNeeded, it may NOT work other times in your app, depending on what's going on in the app.
In short you must always add layoutIfNeeded, that's all there is to it.
setNeedsLayout wont work, you need to layoutSubviews().
UIView.animate(withDuration: 0.2,
delay: 0.0,
animations: {
self.myConstraint.constant = value
self.view.layoutSubviews()
}
Related
#objc private func updateCountdownLabel(_ notification: NSNotification) {
self.progressWidthAnchor.constant = 0
if let timeRemaining = notification.userInfo?["timeRemaining"] as? Int {
self.secondsRemaining = timeRemaining
self.animateProgress(width: 100, duration: 10)
}
}
private func animateProgress(width: CGFloat, duration: Int) {
self.layoutIfNeeded()
UIView.animate(withDuration: TimeInterval(duration),
delay: TimeInterval(LocalConstants.animationDelayDuration),
options: .curveLinear) {
self.progressWidthAnchor.constant = width
self.layoutIfNeeded()
}
}
I'm having a weird issue with my animation. At the moment I am trying to animate a bar decreasing/increasing but I need it to repeat every second.
For some reason it doesn't repeat the animation, the constraint jumps outside the view.
It works the first time, but the second time it doesn't work as you'd expect.
No constraint warnings are thrown.
Moving code around,
updating the layoutIfNeeded to various locations
The cause of my animation not working correctly was due to
removeAllAnimations() not being called prior to the next UIView.animate code blo
Figured it out
Calling removeAllAnimations before executing the code again fixed my issue
I am trying to animate a cover view in my code. Essentially the cover slides up (closes) from off-screen to on-screen, and after the user presses a button the cover slides down (opens up) all the way off-screen.
My code to slide the cover view up the screen and into view is working perfectly. However, the code to slide the cover view down the screen and out of view is not working! The starting position for the cover sliding down is what seems to be incorrect. The cover slides down as expected, but starts far too high, such that the animation ends where it should start.
I've got a screen recording of the error in action (obviously slowed the animation down so you can see what I mean by the error). The cover sliding down should be sliding down from its original position where it slid up to.
Link to recording.
Can anyone spot where I'm going wrong in my code?
func showScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
self.ScoresCoverView.frame.origin.y += verticalDistance
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseOut,
animations:
{ self.ScoresCoverView.frame.origin.y -= self.verticalDistance},
completion:
{ finished in
print("Score cover closed.")
})
}
func hideScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseIn,
animations:
{ self.ScoresCoverView.frame.origin.y += self.verticalDistance},
completion:
{ finished in
print("Score cover opened.")
})
}
EDIT
hey #Nickolans thanks for your help on this issue. So... I have a bizarre update for you on this query. I implemented your code, and it works if I have a pre-programmed button which fires the showScoreCover and hideScoresCover. Turns out even my original code works, but again, only if I have a pre-programmed button which fires the show/hide functions.
However, I need to fire the function using a button where I programmatically reconfigure what the button does. How I've implemented this is through .addTarget and a number of selectors, and those selectors will differ based on where in the game the users are.
Anyway, this is what my code looks like:
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
Originally I only used one .addTarget and included the hideScoresCover() in the startTeamB function. This was resulting in the original issue where the cover doesn't hide correctly (starts too high on the screen). So I split it up into the above two .addTargets, and that also results in the same issue. HOWEVER, if I comment out the entire .addTarget line above related to the startTeamB selector and leave the selector related to hideScoresCover, then the cover is shown and is hidden absolutely as I want to to be, the animation is perfect. This means the underlying code in hideScoresCover() is working fine, but that the issue is arising somehow in how hideScoresCover is being run when the continue button is pressed.
Do you have any idea why this would be? somehow that continueButton target is firing incorrectly whenever there are two addTargets, or when there is only one target but hideScoresCover() is included in startTeamB().
I'm stumped as to why this would be the case. startTeamB() is not a complex function at all, so shouldn't be interfering with hideScoresCover(). So this leads me only to conclude it must be the way the continueButton fires hideScoresCover() when its either a separate target in addition to startTeamB being a target, or when hideScoresCover() is included in startTeamB itself. Unusual that it does work correctly when the only .addTarget is a selector calling hidesScoresCover?
#objc func startTeamB() {
self.UpdateTeam() //Just changes the team name
self.TeamBTimer() //Starts a timer to countdown teamB's remaining time
self.TeamAIndicator.isHidden = true //Just hides teamA image
self.TeamBIndicator.isHidden = false //Just shows teamB image
print("Hintify: ","-----Start Team B.")
}
I took it into Xcode and set it up myself. I believe the issue may be how you're changing your Y value, instead of changing the origin change the view layers y position.
The following code works so you can just take it! Make sure to change the frame to whatever you need it to be when you first start.
class ViewController: UIViewController {
//Bool to keep track if view is open
var isOpen = false
var verticalDistance: CGFloat = 0
var ScoresCoverView = UIView()
var button = UIButton()
override func viewDidLoad() {
//Setup Score view
ScoresCoverView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width)
ScoresCoverView.backgroundColor = .blue
view.addSubview(ScoresCoverView)
//Set vertical distance
verticalDistance = ScoresCoverView.frame.height
//TEST button to test view animations
button.frame = CGRect(x: 200, y: 500, width: 100, height: 100)
button.backgroundColor = .purple
button.addTarget(self, action: #selector(hasBeenFired), for: .touchUpInside)
self.view.addSubview(button)
}
//When button pressed
#objc func hasBeenFired() {
if isOpen {
self.hideScoresCover()
self.isOpen = false
} else {
self.showScoresCover()
self.isOpen = true
}
}
//Show
func showScoresCover() {
self.ScoresCoverView.isHidden = false
UIView.animate(withDuration: 0.5, delay: 0.25, options: .curveEaseOut, animations: {
self.ScoresCoverView.layer.position.y -= self.verticalDistance
}) { (finished) in
print("Score cover closed.")
}
}
//Hide
func hideScoresCover() {
UIView.animate(withDuration: 0.5, delay: 0.25, options: .curveEaseIn, animations: {
self.ScoresCoverView.layer.position.y += self.verticalDistance
}) { (finished) in
self.ScoresCoverView.isHidden = true
print("Score cover Opened.")
}
}
}
EDIT
If you'd like to change the functionality of the button, make sure to remove the previous target before adding a new one.
For example, if you want to switch from show/hide to teamB remove show/hide and add teamB-- and vice versa.
When you first start, add your initial target:
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
Switch to startTeamB:
self.continueButton.removeTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
Switch to hideScoreCover:
self.continueButton.removeTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
I just want to say one thing, maybe the += is not necessary here.
func showScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
self.ScoresCoverView.frame.origin.y += verticalDistance
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseOut,
animations:
{ self.ScoresCoverView.frame.origin.y -= self.verticalDistance},
completion:
{ finished in
print("Score cover closed.")
})
}
should be
`self.ScoresCoverView.frame.origin.y = verticalDistance`
I am developing a small application for tvOS where I need to show a UITextView.
Unfortunately sometimes this text can't fit inside the screen, that's why I need a way to scroll to the bottom of it programmatically.
I've tried using the following code:
self.setContentOffset(offset, animated: true)
It's actually working as intended, except for the fact that I need to handle the animation speed, so that the user can actually read; at the moment it's too fast.
This is what I tried to do, but with little success:
let offset = CGPoint(x: 0, y: contentSize.height - bounds.size.height)
UIView.animate(withDuration: 4, animations: {
self.setContentOffset(offset, animated: false)
})
I need a way to keep the text in place and scroll it to show the missing lines.
I am using a UITextView inside an UIScrollView.
Thanks.
If anyone is looking for a solution, this is what I ended up using:
var textTopDistance = 100
//calculate height of the text not being displayed
textNotVisibleHeight = descriptionLabel.contentSize.height - scrollView.bounds.size.height
func scrollText() {
//start new thread
DispatchQueue.main.async() {
//animation
UIView.animate(withDuration: 6, delay: 2, options: UIViewAnimationOptions.curveLinear, animations: {
//set scrollView offset to move the text
self.scrollView.contentOffset.y = CGFloat(self.textNotVisibleHeight - self.textTopDistance)
}, completion: nil)
}
}
It's not perfect I know, but at least it's working as intended.
I might be heading totally wrong direction, but what I'm trying to achieve is to load some view first and invoke some methods that will be running afterwards infinitely, while at the same time giving the User the possibility to interact with this view.
My first guess was to use viewDidApper with UIView.animate, as per below;
class MainView: UIView {
...
func animateTheme() {
//want this to print out infinitely
print("Test")
}
}
class ViewController: UIViewController {
#IBOutlet weak var mainView: MainView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 1.0, delay: 0.0, options: .repeat, animations: {
self.mainView.animateTheme()
}, completion: nil)
}
}
The case is that I can only see a single "Test" printed out but this has to be invoked infinitely. Also, as I mentioned the User needs to have the possibility to interact with the view once everything is loaded.
Thanks a lot for any of your help.
ViewDidAppear will indeed be called latest when the view is loaded, but I don´t think the user can or will have the time to interact before it´s loaded.
If you want to load animations after everything is loaded you could create a timer and wait x seconds before you do that. Below example will be called 10 seconds after initiation.
var timer = Timer()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(animateSomething), userInfo: nil, repeats: false)
}
func animateSomething() {
// Animate here
}
Update
To make an infinite loop you can use the following snippet:
UIView.animate(withDuration: 5.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.view.frame = CGRect(x: 100, y: 200, width: 200, height: 200)
}, completion: nil)
I've done something very similar on one of my ViewControllers that loads a UIWebView. While the webpage is loading I show a UILabel with text "Loading" and every 1.5 seconds I add a period to the end of the text. I think the approach you describe above and mine shown below have the same effect and neither would be considered an infinite loop.
private func showLoadingMessage(){
let label = loadingLabel ?? UILabel(frame: CGRect.zero)
label.text = "Loading"
view.addSubview(label)
loadingLabel = label
addEllipsis()
}
private func addEllipsis() {
if webpageHasLoaded { return }
guard let labelText = loadingLabel?.text else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.loadingLabel?.text = labelText + "."
self.addEllipsis()
}
}
I'm trying animations and I'm facing a big problem: when my animation finished and I do something ( touch the screen, etc), the elements reset their positions to their first position.
I found this: [blog]: Animation Blocks resets to original position after updating text
they say it's because elements have constraints or auto layout so turn it off to fix it but I don't want to turn it off.
Can we update constraints programmatically?
is there an other solution?
here's my animation:
#IBOutlet var tfUser: UITextfiled!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
tfUser.center.x += view.bounds.width
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(2, delay: 1.8, options: [.CurveEaseInOut], animations: {
self.tfUser.center.x += self.view.bounds.width
//self.tfUser.frame = CGRectMake(0, 233, 375 , 89)
// i tried CGRectMake but it delete the animation
}, completion: nil)
}
In comment of my question , #Hardik Shekhat told to use :
self.tfUser.translatesAutoresizingMaskIntoConstraints = true
it works for me !