I want to change my collectionView position when pressing a button. I wrapped the collectionView and Button in a View and used UIView animate to make it move up. It can move up successfully, but when I am scrolling collectionView it's return back to its original position. If I tried many times, it became normal and didn't move back again.
How could i fix this issue? Thank you for any advice.
func openPreview() {
if !isPreviewOpened {
UIView.animate(withDuration: 0.3) {
self.previewView.frame.origin.y -= self.previewView.frame.height
self.previewButton.frame.origin.y -= self.previewView.frame.height
}
isPreviewOpened = true
updateUI()
}
func closePreview() {
if isPreviewOpened {
UIView.animate(withDuration: 0.3) {
self.previewView.frame.origin.y += self.previewView.frame.height
self.previewButton.frame.origin.y += self.previewView.frame.height
}
isPreviewOpened = false
updateUI()
}
}
After asking others, provide my solution to this question, hope it can help someone in the future.
This issue was happened because that the frame I changed conflicting with the auto layout I set earlier in storyboard. According to the document, I have to set translatesAutoresizingMaskIntoConstraints to true to track the frames.
By default, the autoresizing mask on a view gives rise to constraints that fully determine
the view's position. This allows the auto layout system to track the frames of views whose
layout is controlled manually (through -setFrame:, for example).
When you elect to position the view using auto layout by adding your own constraints,
you must set this property to NO. IB will do this for you.
So this is the answer.
func openPreview() {
if !isPreviewOpened {
previewView.translatesAutoresizingMaskIntoConstraints = true
UIView.animate(withDuration: 0.3) {
self.previewView.frame.origin.y = self.view.bounds.maxY - self.previewView.frame.height
self.previewButton.frame.origin.y = self.view.bounds.maxY - self.previewView.frame.height - self.previewButton.frame.height
}
}
}
Related
I have a Header view that contains pager view (FSPagerView), which has an item (FSPagerViewCell) that contains bar chart, 2019, year review texts. I'm trying to scale down Header at certain times with such code:
private func setSelfHeightAnimated(height: CGFloat)
{
UIView.animate(withDuration: 0.2, animations: {
self.selfHeightAnchor.constant = height
self.superview?.layoutIfNeeded()
})
}
private func setPagerHeightAnimated(height: CGFloat)
{
UIView.animate(withDuration: 0.2, animations: {
self.pagerHeightAnchor.constant = height
self.layoutIfNeeded()
// self.pagerView.layoutIfNeeded()
// self.pagerView.layoutSubviews()
// self.pagerView.cellForItem(at: 0)?.layoutIfNeeded()
// self.pagerView.cellForItem(at: 0)?.setNeedsDisplay()
// self.superview?.layoutIfNeeded()
// self.setNeedsDisplay()
// self.pagerView.setNeedsDisplay()
})
}
However, the old frame glitch is very visible. What could be missing? Here is the video: https://streamable.com/owsdi
You have an 1st layout and a 2nd layout, and an animation that leads from the 1st to the 2nd.
Currently, the 1st layout is faded out and the 2nd layout is faded in during the animation, which gives you the „glitch“.
I assume what you want to achieve is:
- The top part (History, 2019, Year review) should keep their height, and
- only the columns below should shrink/expand, while the lower table slides up or down.
A solution probably is that you animate only the height of a subview that contains the 2 columns.
If the header view is set to adopt its height to its contents, then I expect that the required effect is shown without a glitch.
EDIT:
If you definitively had to animate the upper part as a whole, one possibility may be (that I consider as ugly) to set the alpha value of History, 2019, and Year to 0 immediately before the animation begins. Then these texts would only fade in so the glitch with double images at different places would be avoided.
Try like this to avoid glitching.
private func setSelfHeightAnimated(height: CGFloat)
{
self.selfHeightAnchor.constant = height
UIView.animate(withDuration: 0.2, animations: {
self.layoutIfNeeded()
self.viewDidLayoutSubviews()
})
}
private func setPagerHeightAnimated(height: CGFloat)
{
self.pagerHeightAnchor.constant = height
UIView.animate(withDuration: 0.2, animations: {
self.layoutIfNeeded()
self.viewDidLayoutSubviews()
})
}
Numerous tutorials on animating AutoLayout constraints suggest to update constant property of a constraint and then call layoutIfNeeded() in animation block.
My situation is a bit tricky.
I have a view that houses 3 subviews. The height of this superview is not fixed - it is calculated as a sum of heights of its subviews.
On some event, I ask one of those 3 subviews to toggle its height (it changes between 0 and 30, i.e. I want to smoothly hide and show it).
The code is similar to this:
// In superview
subview1.isVisibleInContext = !subview1.isVisibleInContext
// In subview
class MySubview: UIView {
#IBOutlet var heightConstraint: NSLayoutConstraint!
var isVisibleInContext = false {
didSet {
updateHeight()
}
}
func toggleHeight() {
let newHeight = isVisibleInContext ? 30 : 0
layoutIfNeeded()
heightConstraint.constant = newHeight
UIView.animate(withDuration: 0.8) {
self.layoutIfNeeded()
}
}
}
Unfortunately, this does not work as I expect.
I can see the smooth change of the height of my subview, but the height of my superview is recalculated immediately after I set the new value for my subview height constraint.
I want to see the height of the superview gradually increasing/decreasing as on of its subviews grows or decreases.
Please someone point me in the right direction. Any help is appreciated.
Thanks a lot!
The animation block should be in the UIView that contains the 3 MySubviews. Inside the MySubview class you only update the height constraint's constant:
In Subview
func toggleHeight() {
let newHeight = isVisibleInContext ? 30 : 0
heightConstraint.constant = newHeight
}
Then in the UIView that contains the 3 MySubviews you animate the change:
In Superview
func toggleHeight(with subview: MySubview) {
subview.toggleHeight()
UIView.animate(withDuration: 0.8) {
self.view.layoutIfNeeded()
}
}
The first thing, that was incorrect in my approach was executing self.layoutIfNeeded(). Having investigated the issue I learned out that I had to execute it on the superivew, like this:
self.superview?.layoutIfNeeded()
That also didn't work out for a reason. The main issue in this case was that the view, which had 3 subviews inside was itself a subview of view. So to make it work I had to use the following code:
self.superview?.superview?.layoutIfNeeded()
Definitely not good that subview has to know the hierarchy of views, however it works.
I have a simple UI (very similar to Uber) where the user can scroll a table view on top of the main content.
The UI and the corresponding UITableView animation bug can be displayed as:
Here is what is happening:
User taps on the table view and the table view is expanded. This is done via adding the layout constraint that makes sure that tableView.top = topMenu.bottom. The constraint that gets removed is tableView.height = 30. Everything looks good.
User then scrolls down a certain amount (55+ pixels) and the constraints are reverted back to their original states. This happens inside an animation block so that the flow looks smooth.
The bug occurs here. As far as I understand, the tableView's visible area is calculated of how it will look like after the animation ends. However, this calculation happens before the animation. Therefore, during the animation only 1-2 cells are displayed on the table view; causing the bug.
I can have a workaround here by temporarily setting the tableView's height to a large value and only setting it back to a small value after the animation ends. However, that doesn't work because the safe area on iPhoneX gets covered by the tableView.
Relevant code is here:
private func animateTheChange() {
UIView.animate(withDuration: 0.8, animations: {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded()
})
}
override func updateViewConstraints() {
self.touchConstraints()
super.updateViewConstraints()
}
private func touchConstraints() {
if self.collapsed {
self.view.addConstraint(self.collapsedConstraint)
self.view.removeConstraint(self.expandedConstraint)
if UserHardware.IS_IPHONE_X {
self.bottomConstraint.constant = 0
}
}
else { // Expand
self.view.addConstraint(self.expandedConstraint)
self.view.removeConstraint(self.collapsedConstraint)
if UserHardware.IS_IPHONE_X {
self.bottomConstraint.constant = 34
}
}
}
Relevant Stackoverflow questions (that help but don't solve the issue):
UITableView frame height animation glitch
Dynamic UITableView height
UITableView frame change animation issue
One option...
Embed your tableView inside a "containing" UIView
Constrain the tableView to Top and Bottom of the containing view
Constrain the containing view Bottom to the Safe Area Bottom
Constrain the containing view Top to the Bottom of topMenu with a Priority of 250 (default low), and connect it to #IBOutlet var tableContainerTop: NSLayoutConstraint!
Constrain the Height of the containing view to 30 with a Priority of 750 (default high), and connect it to #IBOutlet var tableContainerHeight: NSLayoutConstraint!
When you want to "expand" or "collapse" your tableView, change the priorities of the containing view's constraints.
UIView.animate(withDuration: 0.8, animations: {
if self.isExpanded {
self.tableContainerHeight.priority = .defaultHigh
self.tableContainerTop.priority = .defaultLow
} else {
self.tableContainerHeight.priority = .defaultLow
self.tableContainerTop.priority = .defaultHigh
}
self.view.layoutIfNeeded()
})
I have a viewcontroller with 3 UIViews that are stacked on top of each other. By stacked I mean at the bottom of one view, the next view begins. I have placed vertical constraints between each view with constant = 0. When the application begins, in viewDidLoad, I'm adding 500 to the vertical constraint between the two top views, so the bottom two views are pushed down below by doing:
billViewBottomConstraint.constant = 500
I then call the following function to animate the two bottom views moving back up, ending right below the top view:
func animate()
{
self.billViewBottomConstraint.constant = 0
UIView.animate(withDuration: 2.0) {
self.view.layoutIfNeeded()
}
}
The views certainly animate to the right position, but not the way I want. It looks like before the views animate, they are expanded outwards and when the animation is called they contract up and inwards towards the right position.
Inside viewDidLoad, the layout is not ready to be animated. You should wait at least until viewDidLayoutSubviews to properly animate constraints. Check with a boolean to make sure it runs only for the first time.
fileprivate var firstLayoutSubviewsTime = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if firstLayoutSubviewsTime {
firstLayoutSubviewsTime = false
animate()
}
}
You should call view.layoutIfNeeded() before the animation block to make sure all views are loaded properly.
You should also modify the constant inside the animation block, not before it.
func animate() {
self.view.layoutIfNeeded()
UIView.animate(withDuration: 2.0) {
self.billViewBottomConstraint.constant = 0
self.view.layoutIfNeeded()
}
}
I've been trying to figure out a way to set the cursor to the bottom of a long UITextView with animation. Setting it wasn't too difficult, in fact this answer covers it very well. https://stackoverflow.com/a/34922332/563381
However, animating it isn't so easy. There isn't a setSelectedTextRange(animated:) that I can find. Setting it inside a UIView.animate... block doesn't seem to do anything. The best I've been able to come up with is to animate the scroll manually then in the completion set the selectedTextRange. However this is fragile, often looks choppy, and seems on occasion to not work at all.
When you set selectedTextRange it does jump to that location. If there was a way to avoid that jump the animation might be smoother and at least would be less fragile since it wouldn't require a delay and you could use setContentOffset(animated) without needing to wait to set the selectedTextRange.
The other option is to find a way to cause the selectedTextRange jump to be animated itself. On this front I tried the trick of disabling scrolling before and reenabling after but that didn't seem to work for me. Guessing that has changed in later versions of iOS.
You can use setContentOffset(_, animated:) and detect the end of the animation with scrollViewDidEndScrollingAnimation and set the cursor like so:
// The action to scroll down
#IBAction func test(_ sender: Any) {
let offset = self.textView.contentSize.height - self.textView.frame.height
self.textView.setContentOffset(CGPoint(x: 0, y: offset), animated: true)
}
// Setting the cursor down
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
let offset = self.textView.contentSize.height - self.textView.frame.height
if scrollView.contentOffset == CGPoint(x: 0, y: offset) {
let newPosition = textView.endOfDocument
self.textView.selectedTextRange = self.textView.textRange(from: newPosition, to: newPosition)
}
}
You will need to add UITextViewDelegate to your view controller and set the textView delegate:
textView.delegate = self