I set the constraints of the view I dragged on to the storyboard so that the view reaches over the whole screen. (I pinned it 0 from the top, left, bottom and right). When the view appears I want the view to shrink. The constant should animate from 0 to 20.
In the app, it changes the constant, but it does not animate it. Also, behind that shrunk view, everything is black although it should be white (because behind that view is the main view which is white).
override func viewDidAppear(animated: Bool) {
UIView.animateWithDuration(2.5, animations: {
self.leftPin.constant = 20.0
self.topPin.constant = 20.0
self.rightPin.constant = 20.0
self.bottomPin.constant = 20.0
// bg is my view I want to shrink
self.bg.layoutIfNeeded()
})
}
I read that in order to "apply" new constraints to the actual layout, You have to run layoutIfNeeded(). But those constraints above also work without that function. Why?
#try this
override func viewDidAppear(animated: Bool) {
self.leftPin.constant = 20.0
self.topPin.constant = 20.0
self.rightPin.constant = 20.0
self.bottomPin.constant = 20.0
UIView.animateWithDuration(2.5, animations: {
self.view.layoutIfNeeded()
})
}
Related
We are currently working in an older codebase for our iOS application and are running into a weird bug where the UIScrollViews paging is not matching on the initialization but only once a user selects the button to change the view.
Expected Result:
The result we have:
Each ScrollView has three slides nested inside of them. We initialize the ScrollView like this:
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("DIScrollView", owner: self, options: nil)
contentView.frame = self.bounds
addSubview(contentView)
contentView.autoresizingMask = [.flexibleHeight,.flexibleWidth]
contentView.layer.borderColor = UIColor.white.cgColor
contentView.layer.borderWidth = 2.0
scrollView.delegate = self
setUpScrollViewer()
}
You can see we call to set up the ScrollView and that is done like this:
public func setUpScrollViewer() {
let slides = self.createSlides()
let defaultIndex = 1
scrollView.Initialize(slides: slides, scrollToIndex: defaultIndex)
pageControl.numberOfPages = slides.count
pageControl.currentPage = defaultIndex
}
Now that all the content is available for each slide, we want to handle the content and we do so with a ScrollView extension:
extension UIScrollView {
//this function adds slides to the scrollview and constraints to the subviews (slides)
//to ensure the subviews are properly sized
func Initialize(slides:[UIView], scrollToIndex:Int) {
//Take second slide to base size from
let frameWidth = slides[1].frame.size.width
self.contentSize = CGSize(width: frameWidth * CGFloat(slides.count), height: 1)
for i in 0 ..< slides.count {
//turn off auto contstraints. We will be setting our own
slides[i].translatesAutoresizingMaskIntoConstraints = false
self.addSubview(slides[i])
//pin the slide to the scrollviewers edges
if i == slides.startIndex {
slides[i].leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
} else { //pin each subsequent slides leading edge to the previous slides trailing anchor
slides[i].leadingAnchor.constraint(equalTo: slides[i - 1].trailingAnchor).isActive = true
}
slides[i].topAnchor.constraint(equalTo: self.topAnchor).isActive = true
slides[i].widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
slides[i].heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
}
//the last slides trailing needs to be pinned to the scrollviewers trailing.
slides.last?.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
self.scrollRectToVisible(CGRect(x: frameWidth * CGFloat(scrollToIndex), y: 0, width: frameWidth, height: 1), animated: false)
}
}
I have tried manually setting contentOffset and nothing seems to be adjusting on the initialization. If the user selects the button it hides and then unhides it to display it properly with no logic adjusting this. Giving me the impression this issue is on the init.
Summary:
When the main view loads, the scrollView is showing me the first slide in the index when i need to be focused on the second slide. However if the user hides and then unhides the scrollView it works as intended.
How do i get the UIScrollView to actually load and initialize updating the scrollView to show the second slide and not initialize on the first slide?
Try explicitely running the scrollRectToVisible in the main thread using
DispatchQueue.main.async {
}
My guess is that all this code runs before the views are positioned by the layout system, and the first slide’s frame is the default 0 x 0 size. When the app returns to this view auto layout has figured out the size of this slide, so the calculation works.
Tap into the layout cycle to scroll to the right place after the layout. Maybe override viewDidLayoutSubviews() to check if it’s in the initial layout and then set the scroll position.
Use constraints for your contentView instead setting frame and autoresizingMask.
Call view.layoutIfNeeded() in the viewController before scrollRectToVisible or setContentOffset(I prefer the last)
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 have an UIView inside my normal UIView. This is my code:
override func viewDidAppear(_ animated: Bool) {
print(canvasUnder.frame.origin.y)
let getRelativePosition = view.frame.size.height * 0.25
canvasUnder.frame.origin.y = canvasUnder.frame.origin.y + getRelativePosition
self.view.layoutIfNeeded()
print(canvasUnder.frame.origin.y)
print(getRelativePosition)
}
It does however stays at the original position. This is my print:
240.0
320.0
80.0
How can this be? Thank you. edit: this is what I want: My UIView that I want to change has a height of * 0.25 of the root view. I want that my UIView is right off the boundaries of my root view, so it needs to be 25% of the root views height, followed by a constrained movement that will push my UIView down.
Edit 2: I managed to to this, this way:
override func viewDidLayoutSubviews() {
print("called")
let getRelativePosition = view.frame.size.height * 0.25
self.CanvasUnder.frame.origin.y = self.CanvasUnder.frame.origin.y + getRelativePosition
}
However this method get called each and every time something changed. I just want that my canvasUnder is just right off the screen when the view is presented. Then, whenever I want, I want to animate that UIView to pop up. I want to use this code:
let getRelativePosition = view.frame.size.height * 0.25
self.CanvasUnder.frame.origin.y = self.CanvasUnder.frame.origin.y - getRelativePosition
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
})
Or putting the change of frame inside the animate function will not work. It just keeps triggering the viewDidLayoutSubviews method which will hide again my UIView.
So how can I just hide that view right under my root view, and pop it up with an slide up animation which will take 1 second?
"this is what I want: My UIView that I want to change has a height of
* 0.25 of the root view. I want that my UIView is right off the boundaries of my root view, so it needs to be 25% of the root views
height, followed by a constrained movement that will push my UIView
down."
This is exactly what auto layout and constraints are for, so you don't have to constantly be calculating sizes.
Use constraints to "pin" your view to left, right and bottom of its superview, then set it's Height Equal to Superview Height, with a multiplier (ratio) of 1:4
That will keep its height at 25% of the "root view" and will keep it "stuck" to the bottom.
No code needed :)
To animate the view in-and-out, add an IBOutlet to the Bottom constraint, and use this code...
class TestViewController: UIViewController {
#IBOutlet weak var trayBottomView: UIView!
#IBOutlet weak var trayBottomConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// hide the "tray" view
trayBottomView.isHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// constraints and frame sizes are fully calculated by AutoLayout here, so...
// move the tray offscreen (below the view), and "un-hide" it
self.trayBottomConstraint.constant = -self.trayBottomView.frame.height
self.trayBottomView.isHidden = false
// this first part will just put the tray view into position, below the screen
UIView.animate(withDuration: 0.01, animations: {
self.view.layoutIfNeeded()
}, completion: { (finished: Bool) in
// now, set the tray Bottom constraint to 0, so it will end up "sitting" on the bottom of the screen
self.trayBottomConstraint.constant = 0
// animate it into view - use delay to "wait a bit" before sliding the view up
// duration of 0.75 (3/4 of a second) may be too slow, just tweak as desired
UIView.animate(withDuration: 0.75, delay: 0.25, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonTapped(_ sender: Any) {
// if the tray Bottom Constraint is Zero, that means it is visible, so
// set it to -(its own height) to position it offscreen, below the view
// otherwise, it is already offscreen, so set it to Zero to bring it back up
if self.trayBottomConstraint.constant == 0 {
self.trayBottomConstraint.constant = -self.trayBottomView.frame.height
} else {
self.trayBottomConstraint.constant = 0
}
// animate it in or out of view
// duration of 0.75 (3/4 of a second) may be too slow, just tweak as desired
UIView.animate(withDuration: 0.75, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.view.layoutIfNeeded()
})
}
}
Any reasons why my UIView does not change if I set different constraints?
You are not changing constraints. You are saying canvasUnder.frame.origin.y. That is not a change of constraint. It is a change of frame. But you cannot directly change the frame if the view is positioned by constraints! The constraints are what positions the view, not the frame.
How can this be?
Because you change the frame, and it does change just at that little moment. But by the time you see the view, the constraints have changed it back again!
When you call self.view.layoutIfNeeded() it is triggering your view to relayout. It is being moved either by auto layout, or layout code in your viewWillLayoutSubviews/viewDidLayoutSubviews functions.
Rather then updating the frame directly, either update the constraint, eg:
canvasUnderHeightConstraint.constant = view.frame.size.height * 0.25
Or, a better solution would be to use the multiplier field of the constraint to get your desired result.
NSLayoutConstraint(item: view,
attribute: .height,
relatedBy: .equal,
toItem: canvasUnder,
attribute: .height,
multiplier: 0.25,
constant: 0)
I want to:
Grow a UISearchBar to the left 93 pixels
Maintain it docked to the right
Do a soft animation while enlarging the view only to the left
This is how to overcome enlargement just to the Left side in swift
(This code will enlarge/shrinks the searchBar 93 pixels over the left side when user start/end editing)
SharedNavigationbBar is the UIView that implements the UISearchBarDelegate
searchBarWidth is an outlet to a constrain holding the width of the UISearchBar
An autolayout constrain must exists on your storyboard or nib file to allow resizing into the left side.
In this case, the neighbord left component is an UIButton.
Add the following code as an extension or inside your class to perform the animated resizing.
extension SharedNavigationBar: UISearchBarDelegate
{
//amount of pixels to enlarge to the left
private var offsetSearchBarLeft:CGFloat
{
get {
return 93
}
}
///Enlarges search bar
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
self.animateSearchBar(self.searchBar, enlarge: true)
}
///Shrinks search bar
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
self.animateSearchBar(self.searchBar, enlarge: false)
}
//shrinks or enlarge the searchbar (this will be the function to call inside the animation)
private func animateSearchBar(searchBar:UISearchBar, enlarge:Bool)
{
///Important here, for this to work, the option and the searchbar size must be handled this way
UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.LayoutSubviews, animations: { [weak self] () -> Void in
let multiplier: CGFloat = enlarge ? 1 : -1
let origin = searchBar.frame.origin.x + self!.offsetSearchBarLeft * multiplier
let width = searchBar.frame.width + self!.offsetSearchBarLeft * multiplier
//This Block of code, setting the new frame, needs to be inside the animation in order to work
var newBounds:CGRect = searchBar.frame;
newBounds.origin.x = origin
newBounds.size.width = width
//Sets the new frame
self?.searchBarWidth.constant = width
searchBar.frame = newBounds
}, completion: nil)
}
}