Background
I'm trying to do a fairly standard NSLayoutConstraint constant update, with both an animated and non-animated option. To give you some background, I have a UIView subclass called ProgressView. Within the ProgressView, there is the Progress Bar UIView. The Progress Bar UIView is connected as a referencing outlet progressBar to the ProgressView class. Here's how the fairly simple hierarchy looks within the Storyboard:
As you might have guessed, I'm building a custom progress bar. The ProgressView UIView is the "track," and the Progress Bar UIView is the "fill." The plan is to use the trailing constraint of the Progress Bar UIView to make changes to the progress.
Existing configuration
The constraints for the Progress Bar UIView are set up to be flush with the superview (Progress View):
The trailing constraint of the Progress Bar UIView is connected to the ProgressView class as a strong outlet: #IBOutlet var trailingConstraint: NSLayoutConstraint!.
Within the ProgressView class, there is a setProgress function. This function is called from the ViewController which contains the ProgressView. Here's the function with comments explaining how it works:
public func setProgress(_ progress: Double, animationDuration: Double) {
// Pre-conditions
guard progress >= 0.0 && progress <= 1.0 && animationDuration >= 0.0 else {
return
}
// Calculate the new constraint constant based on the progress
let newConstant = CGFloat(1 - progress) * self.bounds.size.width
// If the update should be made without animation, just make the change and return
guard animationDuration > 0.0 else {
trailingConstraint.constant = newConstant
return
}
// Set the constraint first
self.trailingConstraint.constant = newConstant
// Animate!
UIView.animate(withDuration: animationDuration) {
self.layoutIfNeeded()
}
}
The problem
As you can see, this function can update the progress with and without animation. However, when called from the ViewController, neither works. The trailing constraint constant appears to remain 0, and the progress bar fills the entire track.
Attempted solutions
I have tried calling layoutIfNeeded() and setNeedsLayout() in every position and configuration where it makes sense (on both self [where self is the ProgressView] and on progressBar). I have tried putting the code explicitly on the main thread with DispatchQueue.main.async. As a ground truth test, I tried connecting the trailingConstraint outlet to the ViewController itself and updating the constant from the viewDidAppear() and viewDidLayoutSubviews() methods. Nothing works.
A possible hint in the right direction
Here's the weird part. The progress bar appears filled on the screen, but when I debug the view hierarchy, the progress bar looks correct. The trailingConstraint constant seems to have been set correctly.
I was testing on an iOS 10 device and running Xcode 8.3.3. This issue seems really odd to me, and I'm not quite sure what to do from here. Thanks for your help.
I faced the same problem.My problem was solved by changing constant in animation .Try this out:
UIView.animate(withDuration: 0.2) {
self.trailingConstraint.constant = newConstant
self.layoutIfNeeded()
}
Related
I am designing an iOS app in swift, and I am having some difficulty with animations during a controller transition. Specifically, I've implemented a UINavigationControllerDelegate, to listen for when a certain view is pushed. When this view is pushed, I want to hide a bar at the bottom of the screen. My code is working almost perfectly, however whenever I begin an animation on the height of the navigation controller, the current view (which is being removed) animates its height correctly, but the new controller which is being pushed already has the new height from the animation. To put some code to it, the following function is called from my UINavigationControllerDelegate's willShow viewController function:
func animatePlayerVisibility(_ visible: Bool) {
if visible == showingPlayer {
return
}
showingPlayer = visible
let height: CGFloat = visible ? 56.0 : 0.0
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.35) {
self.playerHeight.constant = height
self.viewBottom.constant = height
self.view.layoutIfNeeded()
}
}
'playerHeight' is an IBOutlet to a constraint on the height of the player container view. 'viewBottom' is also an IBOutlet constraint between the bottom of the top container view and the bottom of the screen. Essentially, as long as these two constraints are animated together, it should look nice.
To help visualize the graphical bug, I edited this line
self.viewBottom.constant = height
to
self.viewBottom.constant = height * 2.0
I have created an imgur album of the actual wrong behavior in action:
http://imgur.com/a/znAim
As you can see, the old view controller animates properly, when the new controller already has the new animated size.
Here is the layout of my storyboard:
Any help would be really appreciated. I've been trying to fix this for a while with no success.
EDIT: The view of the animation without the *2 applied.
https://imgur.com/a/2a5Sw
Have you thought about not using UINavigationController? Maybe it will be easier to use ChildViewControllers mechanism. Then with it you can use a powerful autolayouts and have more control over animation (in your case height)
More info about this here
I've created a nice little sample project you can find here!
There are a number of things that could be going wrong, and since I haven't looked over your project personally it's likely I organized things very differently in my sample, but hopefully you will understand it. I think the big thing is that I added a constraint in storyboard to the navigationController's container to the bottom of the root viewController. I don't adjust the height of this container at all when I animate.
If I try to animate the hiding all the subviews of a stackview, I can see them moving towards the top left corner. On showing, they are animated coming from top left to their proper space.
If I hide only a subset of the arranged views, they are animated as expected.
My current workaround is to keep an invisible subview in the stack, but this is super wonky.
I am hiding via
UIView.animate(withDuration: 0.5) {
self.someStack.arrangedSubviews.forEach { $0.isHidden = !$0.isHidden
}
Try adding an additional empty view (width/height 0) into your stack view. This fixed the issue for me.
I faced a very similar problem and after a few hours of back and forth, I found that calling self.view.layoutIfNeeded() fixed the issue.
My hierarchy:
- UIView
- UIStackView
- UIStackView(1)
- UIButton
- UIButton
- UIStackView(2)
- UITextField
- UIButton
- UIActivityIndicatorView
The root UIView animates from the bottom when the keyboard appears based on a call to UITextField.becomeFirstResponder() in the (2)UIStackView. By default, every subview of (2)UIStackView is hidden. Based on a UISegmentedControl change, the app calls UITextField.becomeFirstResponder() and hides (1)UIStackView and shows (2)UIStackView. If I don't call self.view.layoutIfNeeded() after stackView.subviews.forEach { $0.isHidden = false } to show the subviews of (2)UIStackView I see them animating from the top left of the device.
I don't know if this might help you, but it might be a starting point to investigate.
I have two relatively simple animations. One is animating the top constraint of a UIButton so that it slides up. The other is animating the background color on a UIView.
self.buttonAnimationConstant.constant = 0
self.view.layoutIfNeeded() // ensure the constraint is at 0
self.buttonAnimationConstraint.constant = 100
UIView.animateWithDuration(0.25, delay: 0.0) {
self.view.layoutIfNeeded()
}
and my color animation:
UIView.animateWithDuration(0.25, delay: 0.0) {
self.colorView.backgroundColor = UIColor.purpleColor()
}
If I try executing them at the same time, then the button will animate up but it cancels the color animation, presumably because of view.layoutIfNeeded. Note that these two animations are in separate places so they can't be joined into one block (one sits at the view controller level, the other embedded inside a custom view but both within the same view controller). How can I animate both a constraint and a view property such that one doesn't cancel the other?
Essentially the issue is how do you go about animating both a constraint and a view that sit in the same view hierarchy?
Animation blocks do not cancel each other. The issue here is this line:
self.view.layoutIfNeeded() // ensure the constraint is at 0
Calling layoutIfNeeded outside an animation block will cancel your animations (because they set a new value for the values you animate) and there is not much you can do to prevent that. What you could do is making sure that no layoutIfNeeded is called outside an animation block while performing an animation. Of course this does not happen if the views are not in the same view hierarchy.
I am trying to use a UIScrollView to show a series of UIViews. In my storyboard I have a View Controller containing a UIView that is constrained using AutoLayout.
View Controller (UIView in grey)
In order to call the UIScrollView I am using the following method:
func initScrollview() {
self.mainScrollView = UIScrollView(frame: self.mainView.bounds)
self.mainScrollView!.contentSize = CGSizeMake((self.mainView.bounds.width)*CGFloat(3), self.mainView.frame.height)
self.mainScrollView!.backgroundColor = UIColor.greenColor() // For visualization of the UIScrollView
self.mainScrollView!.pagingEnabled = true
self.mainScrollView!.maximumZoomScale = 1.0
self.mainScrollView!.minimumZoomScale = 1.0
self.mainScrollView!.bounces = false
self.mainScrollView!.showsHorizontalScrollIndicator = true;
self.mainScrollView!.showsVerticalScrollIndicator = false;
self.mainScrollView!.delegate = self
for i in 0...3 {
var tempView = SubView(frame: self.mainView.bounds)
pages.insert(tempView, atIndex: i)
self.mainScrollView!.addSubview(pages[i]);
}
self.mainScrollView!.scrollRectToVisible(CGRectMake(mainScrollView!.frame.size.width, 0, mainScrollView!.frame.size.width, mainScrollView!.frame.size.height), animated: false)
}
When I run my code, the UIScrollView does not fit the UIView. Instead it is too short and too wide. The result looks like this:
UIView in grey, UIScrollView in green
What am I doing wrong that is causing the UIScrollView to be incorrectly sized?
You should put the codes that you init the UI element sizes base on the screen size(UIView of UIViewController) in viewDidLayoutSubviews. Because in viewDidLoad, the screen didn't adjust its size yet,
In the above code, there no mention of adding the mainScrollView to the mainVew.
To whose view are you adding the mainScrollView? My opinion would be you are trying to add it to self.view whereas it should be to self.mainView
After the initScrollView() function is called try adding it the below code
self.mainView.addSubview(mainScrollView!)
This would probably be easier if you had placed all these views directly in your storyboard instead of programatically. I don't see anything in your code that can't be done visually in IB. Also, if you have autoLayout active in your storyboard, setting frames and sizes in code won't work. (auto-layout will change your values on the next pass)
I created an IBOutletCollection with some UIElements (Label, Button, TextField) that i would like to move on a special event.
thats what I have so far:
#IBAction func moveDown(sender: UIButton) {
UIView.animateWithDuration(0.4, animations: {
for subview in self.moveOutViews {
var offset = CGFloat(700);
var newY = subview.frame.origin.y + offset;
subview.frame = CGRectMake(subview.frame.origin.x, newY, subview.frame.size.width, subview.frame.size.height);
}
}, completion: {
(value: Bool) in
UIView.commitAnimations();
});
}
now my problem is, when the Animation is completed, and i want to interact with the UIElements (TextField) on the new position, all views 'jump' back to the origin position.
maby it's a problem that the 'sender' is part of the IBOutletCollection?
It's an iPad for iOS 8.3 app and i am using Swift.
Edit: I made a workaround with an UIScrollView. But I would like to know why the first idea isn't working.
Since we don't know how your view is setup I will have to guess that it is one of two things:
Like #Paulw11 suggested in his comment. You are editing the frames of views that are held by constraints. When the view is laid out again all views snap back to positions determined by the constraints. Event though you say that you are not using constraints it is possible that translatesAutoresizingMaskIntoConstraints is enabled for your views or that your xib/storyboard is set to use constraints and so constraints are automatically generated for you. One way to check this is to just log the constraints property of a subview.
You have code somewhere else in your app that is changing the position of your views and it is inadvertently being called.