Animate the height of a UIScrollView based on it's content - ios

My situation:
I have a horizontal ScrollView containing a StackView.
Inside this StackView there are some Views, that can be expanded/collapsed.
When I want to expand one of these Views, I first unhide some subViews in the View. After that I need to change the height of the ScrollView based on the new height of this View.
But this is not working...
I try this code:
UIView.animate(withDuration: 0.3) { [self] in
// Toggle hight of all subViews
stackView.arrangedSubviews.forEach { itemView in
guard let itemView = itemView as? MyView else { return }
itemView.toggleView()
}
// Now update the hight of the StackView
// But here the hight is always from the previous toggle
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print(height)
heightConstraint.constant = height
}
This code nicely animates, but always to the wrong height.
So the ScrollView animates to collapsed when it should be expanded and expanded when it should be collapsed.
Anyone with on idea how to solve this?

The problem is that, whatever you are doing here:
itemView.toggleView()
may have done something to change the height a view, but then you immediately call:
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
before UIKit has updated the frames.
So, you can either track your own height property, or...
get the frame heights after the update - such as with:
DispatchQueue.main.async {
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print("h", height)
self.scrollHeightConstraint.constant = height
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}

Related

Get the default shrunk and expanded height of large title navigation bar

I have enabled large titles for the navigation bar with:
navigationController?.navigationBar.prefersLargeTitles = true
This makes the navigation bar start with an expanded height, and shrink as the user scrolls down.
Now, I want to add a subview inside the navigation bar that resizes, based on how tall the navigation bar is. To do this, I will need to get both the maximum and minimum height of the navigation bar, so I can calculate the fraction of how much it's expanded.
I can get the current height of the navigation bar like this:
guard let height = navigationController?.navigationBar.frame.height else { return }
print("Navigation height: \(height)")
I'm calling this inside scrollViewDidScroll, and as I'm scrolling, it seems that the expanded height is around 96 and the shrunk height is around 44. However, I don't want to hardcode values.
iPhone 12
Expanded (96.33)
Shrunk (44)
iPhone 8
Expanded (96.5)
Shrunk (44)
I am also only able to get these values when the user physically scrolls up and down, which won't work in production. And even if I forced the user to scroll, it's still too late, because I need to know both heights in advance so I can insert my resizing subview.
I want to get these values, but without hardcoding or scrolling
Is there any way I can get the height of both the shrunk and expanded navigation bar?
Came across my own question a year later. The other answer didn't work, so I used the view hierarchy.
It seems that the shrunk appearance is embedded in a class called _UINavigationBarContentView. Since this is a private class, I can't directly access it. But, its y origin is 0 and it has a UILabel inside it. That's all I need to know!
extension UINavigationBar {
func getCompactHeight() -> CGFloat {
/// Loop through the navigation bar's subviews.
for subview in subviews {
/// Check if the subview is pinned to the top (compact bar) and contains a title label
if subview.frame.origin.y == 0 && subview.subviews.contains(where: { $0 is UILabel }) {
return subview.bounds.height
}
}
return 0
}
}
Usage:
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Navigation"
if
let navigationBar = navigationController?.navigationBar,
let window = UIApplication.shared.keyWindow
{
navigationBar.prefersLargeTitles = true /// Enable large titles.
let compactHeight = navigationBar.getCompactHeight() // 44 on iPhone 11
let statusBarHeight = window.safeAreaInsets.top // 44 on iPhone 11
let navigationBarHeight = compactHeight + statusBarHeight
print(navigationBarHeight) // Result: 88.0
}
}
The drawback of this answer is if Apple changes UINavigationBar's internals, it might not work. Good enough for me though.
Using following extension u can get extra height
extension UINavigationBar
{
var largeTitleHeight: CGFloat {
let maxSize = self.subviews
.filter { $0.frame.origin.y > 0 }
.max { $0.frame.origin.y < $1.frame.origin.y }
.map { $0.frame.size }
return maxSize?.height ?? 0
}
}
And I said earlier u can get extended height by following
guard let height = navigationController?.navigationBar.frame.maxY else { return }
print("Navigation height: \(height)")
let window = UIApplication.shared.keyWindow
let topPadding = window?.safeAreaInsets.top
let extendedHeight = height - topPadding
You can get shrunk height by subtracting difference from extended height
guard let difference = navigationController?.navigationBar.lagreTitleHeight else {return}
let shrunkHeight = extendedHeight - difference

Animate subview AutoLayout constraint changes

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.

UIScrollView sometimes lose its scroll when changing the height of UITableView inside

I have simple UIScrollView with two subviews: UIView and UITableView set in vertical mode. Both of them have their own height constraint:
I set it like this:
tableViewHeightConstraint.constant = CGFloat(height) //height is calculated based on number of cells multiplied 130
calendarViewHeightConstraint.constant = UIApplication.shared.keyWindow!.rootViewController!.traitCollection.isIpad ? 630 : 470
But sometimes when I scroll to the bottom, and then add a new cell to the table view and recalculate height it seems that scroll of scroll view is locked, and I can bouncing but the scroll disappear and I can only bouncing, although I know that I can scroll to the content above or below the current view on the screen. What may be the reason?
I really have to do this like this. There is no possibility to put my view inside table view header view.
This is where I recalculate height:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
updateView()
updateTableViewHeight()
}
private func updateTableViewHeight() {
var height = goalFetchedResultsController.fetchedObjects!.count * 130
height += 45
if MyType.selected.hasMonthReport {
height += 130
}
if MyType.selected.hasAnnualReport {
height += 70
height += 130
}
tableViewHeightConstraint.constant = CGFloat(height)
}
New cell is added inside delegate of NSFetchedResultsController.
I'm thinking you need to change your calculation way of height of TableView like,
var height = tablView.contentSize.heigh // You can do + or - here
// your other stuff of calculation.
tableViewHeightConstraint.constant = CGFloat(height)
self.view.layoutIfNeeded() // You need to call after change constraint.

How to make UILabel resize it's dimensions correctly when the view width changes?

I have a UILabel configured to automatically size for multi-lines depending on text length. The label is in a dynamically sizing UITableViewCell with constraints on all size.
The code moves UILabel origin to the right, thereby shrinking the UILabel width.
The label correctly resizes and wraps the text. In the process of positioning the origin back to it's original location, the height of the UILabel expands for 3 lines. But only 2 lines of text are required.
The reason the height expands to 3 lines is that in the process of returning the origin back to the original point with view animation, The text expands to 3 lines and reduces back to 2.
Is there a call or setting to make that will have the cell resize correctly? Or 2 is there a way to keep the cell from resizing when he origin changes. In particular. Is it possible to momentarily disable the UILabel multi-line option as the width shrinks and expands?
Here are screenshots showing the behavior.
Figure (1) Position of label in the UITableViewCell before changing its origin.
Figure (2) Origin of UILabel moves to the right and reduces the width of the label. Notice the second row has 2 lines and the label border tightly hugs the text.
Figure (3) Origin returns the position of the first image. Notice the UIlabel of the second row loosely hugs the text and does not return to it's original height.
#IBAction func editTable(_ sender: UIBarButtonItem) {
if isEditingDynamic {
let animate = UIViewPropertyAnimator(duration: 0.4, curve: .easeIn) {
for cell in self.tableView.visibleCells as! [CellResize] {
cell.lblDescription.frame.origin.x = 16
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
animate.addCompletion(){
(position:UIViewAnimatingPosition) in
for cell in self.tableView.visibleCells as! [CellResize] {
cell.leadingContraint.constant = 0
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
animate.startAnimation()
isEditingDynamic = false
} else {
let animate = UIViewPropertyAnimator(duration: 0.4, curve: .easeIn) {
for cell in self.tableView.visibleCells as! [CellResize] {
cell.lblDescription.frame.origin.x = 60
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
animate.addCompletion(){
(position:UIViewAnimatingPosition) in
for cell in self.tableView.visibleCells as! [CellResize] {
cell.leadingContraint.constant = 54
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
animate.startAnimation()
isEditingDynamic = true
}
}
Are you able to post some codes of how your table view cells and controller is set up? Typically, what I will do is I will create a single line UILabel and place it inside the cell's content view, constraining it Top, Left, Trailing & Leading with a height constraint >= current & numberOfLines = 0.
Then, in the tableview cell, I will set its estimatedHeight to be 100 and heightForRow to be UITableViewAutomaticDimension.
In this case, TableView will automatically handle the height of the cell based on the label's contents.

UITableViewCells animating at different rate than collapsing tableHeaderView

I am trying ot troubleshoot a tableView header (pink) that is animating a collapse. As the tableViewHeader height is shrinking the table view cells should pull up with the top of their tableView (orange). The beginning and end states are correct, but somehow the table view cells are animating up at a different rate. Something is clearly wrong here, I just can't seem to pinpoint what it is.
It appears to have something to do with the fact that I am using self sizing table view cells and tableView.rowHeight = UITableViewAutomaticDimension. If I use fixed height cells everything is fine.
Beginning State:
Middle State (Note cells already sliding under header):
Final State (Final state of layout is correct):
Here is the code that animates the collapse.
func collapseHeader() {
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
self.tableView.beginUpdates()
if let header = self.tableView.tableHeaderView as? TopicTableHeaderView {
header.setHeaderState(state: .collapsed)
}
self.sizeHeaderToFit()
self.view.layoutIfNeeded()
self.tableView.endUpdates()
}) { (bool) in
print("collapse completed")
}
}
func sizeHeaderToFit() {
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
if headerView.frame.height != height {
headerView.frame = frame
tableView.tableHeaderView = headerView
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
}
}
}
And the problem was simply where I was calling self.tableView.beginUpdates(). I moved that to the line directly above self.tableView.endUpdates() and that solved the problem.

Resources