I'll get right to the point.
I have a UIViewController that has two subviews in it. The top one (let's call it HeaderView from now one) is a custom UIView and the bottom one is a UITableView.
I have set them up in InterfaceBuilder so that the HeaderView has 0 margin from the left, top and right, plus it has a fixed height.
The UITableView is directly underneath with 0 margin from all sides.
My goal is to achieve a behaviour such that when I start scrolling the UITableView's content the HeaderView will start shrinking and the UITableView becomes higher without scrolling. This should go on until the HeaderView has reached a minimum height. After that the UITableView should start scrolling as normal. When scrolling down the effect should be reversed.
I have initially started this out using a UIScrollView instead of the UITableView and I have achieved the desired result. Here is how:
connect the UIScrollView to the outlet
#IBOutlet weak var scrollView: UIScrollView!
set the UIScrollViewDelegate in the controller's viewDidLoad() method
self.scrollView.delegate = self
and declared the UIViewController to conform to the protocol
intercept when the UIScrollView scrolls:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.adjustScrolling(offset: scrollView.contentOffset.y, scrollView: scrollView)
}
in my adjustScrolling(offset:scrollView:) method the "magic" happens
Now let's look at what happens in this method.
private func adjustScrolling(offset: CGFloat, scrollView: UIScrollView) {
// bind value between 0 and max header scroll
let actualOffset: CGFloat = offset < 0 ? 0 : (offset >= self.maxHeaderScroll ? self.maxHeaderScroll : offset)
// avoid useless calculations
if (actualOffset == self.currentOffset) {
return
}
/**
* Apply the vertical scrolling to the header
*/
// Translate the header up to give more space to the scrollView
let headerTransform = CATransform3DTranslate(CATransform3DIdentity, 0, -(actualOffset), 0)
self.header.layer.transform = headerTransform
// Adjust header's subviews to new size
self.header.didScrollBy(actualOffset)
/**
* Apply the corrected vertical scrolling to the scrollView
*/
// Resize the scrollView to fill all empty space
let newScrollViewY = self.header.frame.origin.y + self.header.frame.height
scrollView.frame = CGRect(
x: 0,
y: newScrollViewY,
width: scrollView.frame.width,
height: scrollView.frame.height + (scrollView.frame.origin.y - newScrollViewY)
)
// Translate the scrollView's content view down to contrast scrolling
let scrollTransform = CATransform3DTranslate(CATransform3DIdentity, 0, (actualOffset), 0)
scrollView.subviews[0].layer.transform = scrollTransform
// Set bottom inset to show content hidden by translation
scrollView.contentInset = UIEdgeInsets(
top: 0,
left: 0,
bottom: actualOffset,
right: 0
)
self.currentOffset = actualOffset
}
If I haven't forgotten anything this should be enough to achieve the desired effect. Let me break it down:
I calculate the actualOffset binding it between 0 and self.MaxHeaderScroll which is just 67 (I think, it's calculated dynamically but this doesn't really matter)
If I see that the actualOffset hasn't changed since the last time this function was called I don't bother to aplly any changes. This avoids some useless calculations.
I apply the scrolling to the header by translating it up with a CATransform3DTranslate on just the y axis by negative actualOffset.
I call self.header.didScrollBy(actualOffset) so that the HeaderView can apply some visual changes internally. This doesn't concearn the question though.
I resize the scrollView so that it keeps 0 margin from top and bottom now that the HeaderView is higher up.
I translate down the scrollView's content by the same actualOffset amount to contrast the scrolling. This piece is essential to the correct visual effect that I want to achieve. If I didn't do this, the scrollView would still resize correctly but the content would start scrolling right away, which I don't want. It should only start scrolling once the HeaderView reaches it's minimum height.
I now set a bottom inset in the scrollView so that I am able to scroll it all the way to the end. Without this, the last part of the scrollView would be cut off since the scrollView itself would think it reached the end of it's content.
Lastly I store the actualOffset for later comparison
As I said, this works fine. The problem arises when I switch from a UIScrollView to a UITableView. I assumed it would work since UITableView inherits from UIScrollView.
The only piece of code that doesn't work is the number 6. I don't really know what is going wrong so I will just list everything I have found out and/or noticed. Hopefully someone will be able to help me out.
in the case of the UIScrollView, in point 6, the scrollView.subviews[0] refers to a view that holds all the content inside it. When I change to UITableView this subview seems to be of the type UITableViewWrapperView which I could not find any documentation about, nor does XCode recognize it as a valid class. This is already frustrating.
if in point 6 I also give some translation on the x axis (let's say of 50) I can see an initial very quick translation that is immediately brought back to 0. This only happens when the UITableView starts scrolling, it doesn't go on while scrolling.
I have tried changing the frame of the subview in point 6 to achieve the desired result. Although the scrolling is correct, the top cells start disappearing as I scroll the UITableView. I thin this is because I am using dequeueReusableCell(withIdentifier:for:) to instatiate the cells and the UITableView thinks that the top cells aren't visible when they actually are. I wasn't able to work around this problem.
I have tried setting the self.tableView.tableHeaderView to a UIView of the actualOffset height to contrast scrolling but this gave a weird effect where the cells would not scroll correctly and when the UITableView was brought back to the initial position, there would be a gap on top. No clue about this either.
I know there's a lot here so please don't hesitate asking for more details. Thank you in advance.
I made something like this recently, so heres how I achieved it:
Make a UIView with a height constraint constant and link this to your view/VC, have you UITableview constrained to the VC's view full screen behind the UIView.
Now set your UITableViews contentInset top to the starting height of your 'headerView' now, in the scrollViewDidScroll you adjust the constant until the height of the header is at its minimum.
Here is a demo
If you just run it, the blue area is your 'header' and the colored rows are just any cell. You can autolayout whatever you want in the blue area and it should auto size and everything
Related
I'm trying to create layout that it structured like this:
- View
-- ScrollView
--- ContentView
---- CustomView
---- CustomView
---- TableView
---- CustomView
The tableView itself is auto-resizable using "invalidateIntrinsicContentSize" and when I add items - the height of the tableview changes, pushing the custom view below it further down.
Once enough items are added I the bottom custom view is hidden and the scroll doesn't work.
important fact - the bottom custom view doesn't have a bottom constraint. It is pushed down by the it's top constraint to the tableView.
If I do set a bottom constraint - the table view will no longer be dynamically resized.
The intended behaviour:
When a user adds items to the list and the list gets too big the ContentView will be scrollable so the user can scroll to see the bottom view.
The actual behaviour:
When a user adds items to the list and the list gets too big, the bottom view is pushed down and outside of sight and content is not scrollable.
What is happening and how can I fix it?
Below is what I think what is happening.
Since you are using UITableView, it has its own scroll view. So when the UITableView list gets too big, UITableView itself becomes scrollable rather than ScrollView's contentView becoming scrollable.
To achieve what you need, you would have to make the UITableView not scrollable and use the intrinsicHeight of the UITableView to get the actual height of UITableView along with all the items. If you have items with varying heights, it will be a problem because you won't know the height before rendering. With same height for all the rows, you can get the total height of the UITableView and set the height constraint to that value. This will increase the contentSize of the outer ScrollView, making it scrollable.
Apart from UITableView, you can also use UIStackView. This is because you are not using the reusing capabilities of UITableView anyways. Managing the datasource and delegates should not be a big problem.
You can create a constraint for tableview height, And take its reference to your swift file, by dragging it as you take other views. Now in your code, Just do this
tableViewHeightConstraint.constant = tableViewNoOfItems * tableViewCellHeight;
if you have set other constraints perfectly inside scrollview, It should work perfectly. Means TableView should have top, bottom, left, right margined constraints from the ScrollView.
try this code
tblViewHeight.constant = CGFloat( tableview row count * 45 )
var size = contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
if size.height < scrollView.frame.size.height
{
size = scrollView.frame.size
}
contenViewHeight.constant = size.height - scrollView.frame.size.height
scrollView.contentSize.height = contenViewHeight.constant
What I think you could do is:
Disable tableView's scroll tableView.isScrollEnabled = false
Every time a user adds items to the list, reload the tableView
Also using UIStackView with vertical axis and .fillEqually distribution as a Content View would be much more convenient as you won't need to set any positional constraints to your views, but may need to set height constraints if intrinsic content size can't be determined by the engine
I have a view at the top of a view controller, and a tableview underneath it.
I've made it such that as the tableview is scrolled up the top view scrolls up too, up to a maximum amount, of lets say 50 points.
The tableview also has a top inset of 50:
tableView = UIEdgeInsets(top: 50, left: 0, bottom: 0, right: 0)
... so that it's cells start below the top view.
And in the scrollview delegate there is some code along the lines of:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollViewYOffset = ...
topViewHeightConstraint.constant = max(minTopViewHeight, minTopViewHeight - scrollViewYOffset)
}
This ensures as the user drags down on the tableview the top view 'sticks' to it, and is also pushed up when the user pushes the table view up.
I've drawn this picture to better describe what the Storyboard looks like:
So far so good. When there are a lot of cells and the user scrolls the table view up, the top view remains at it's minimum height nicely.
But if you are scrolled up - so the top view is at it's minimum - then the number of cells are reduced, the top view pings back down to it's maximum height.
This is because the actual content size of the tableview has dropped below its bounds height, and so as a scrollview it brings the top of the content to the top of the scrollview again (plus the 50 point top inset of course).
I would like to be able to scroll the tableview up, so the top view remains at its minimum height, regardless of the number of cells it contains - i.e. regardless of it's content size.
Can anyone think of a clever way to set a minimum content size on the table view?
(So far I've tried messing around with the footer, having a cell at the bottom that is essentially a spacer - this messes up the tableview's logic and some reordering code I have in there. I've attempted to coerce the offers etc. to my will, but haven't quite worked out how to achieve this.)
I would greatly appreciate some UI genius to point me in the right direction :) Thank you.
UPDATE:
Thank you for all the answers and comments.
After trying various types of footer and header views, tweaking constraints & layout priorities on scroll, adding spacer cells, putting the tableview inside a scrollview, etc. - it finally occurred to me I was making this more complicated than I needed to, and should just update the cause of the problem on the scrollViewDidScroll, the contentInset value.
See the answer below for a code example that achieves the behaviour I was looking for.
As the contentInset is what is causing the tableview to ping back to the wrong point, I simply needed to adjust the content inset as the tableview was scrolled up/down.
Here is some example code of what I did:
let maxPointsTopViewCanMoveUp: CGFloat = 50
let topInset = abs(min(max(-maxPointsTopViewCanMoveUp, scrollView.contentOffset.y), 0))
scrollView.contentInset = UIEdgeInsets(top: topInset, left: 0, bottom: 0, right: 0)
scrollView.scrollIndicatorInsets = UIEdgeInsets(top: topInset, left: 0, bottom: 0, right: 0)
let amountToMoveTopViewUp = maxPointsTopViewCanMoveUp - topInset
topViewToSuperviewTopConstraint.constant = amountToMoveTopViewUp
This is called from the scrollViewDidScroll of the tableview.
It means that when there are too few cells in the tableview to fill the content, the top the tableview sticks in the place it had been scrolled up to (i.e. the amount it had pushed up the top view).
Some ideas that you can try:
Create a "dummy" row on index 0 and make it's height be 50 (it will be hidden below the top view). Maybe you can leave that dummy row in section 0 and the rest of your data in section 1, so you don't have to think about it when deleting your data.
Or, instead of a dummy row, you can set the height of the tableView's header to be 50
Maybe set a minimum height constraint on the tableView, and keep it's content compression resistance priority high.
I have a UIView which contains a label, a button, and a UITableView which populates its data dynamically from a server. I am having trouble resizing the parent UIView to fit its content after the content has dynamically populated. For the purpose of demonstrating my issue, I have made the background of the containing UIView blue.
After populating the TableView with data, the UIView's height does not adjust causing the Tableview data to overflow, seen in the diagram below.
I have set the bottom, leading and trailing space constraints of the TableView to the superview, and top space constraint to the button. The UIView itself has no height constraints set.
I implemented a function to manually recalculate the height of the UIView after populating the content of the TableView. Code for the function below:
func resizeToFitSubviews()
{
var w: CGFloat = self.frame.size.width,
h: CGFloat = 0
for view in subviews {
if view.frame.origin.y + view.frame.height > h { h = view.frame.origin.y + view.frame.height }
}
self.frame.size = CGSize(width: w, height: h)
}
This function works. The UIView resizes to what seems to be the right size, but the TableView disappears after doing so:
Completely lost as to why this occurs. The label and button seem unaffected. I either need to make it so autolayout automatically adjusts the height of the UIView, or make it so that resizing the UIView does not cause the TableView to disappear.
In the View Debugger, the TableView is returning a height of 0 (while the rows are returning 130 as expected given that is what I return in my heightForRowAt function).
Thanks
Don't adjust the view frame if you are using Auto Layout.
Make a height constraint and adjust that to your calculated value.
-
Also it might be easier to manually calculate this height.
height = numberOfRows * heightPerRow
I have a view with an image and a tableview. The image is in the first half of the screen (portrait) and the tableview in the second half. And now I want to move the tableview over the image until it reaches the top and covers the image. Then it should start the "real scrolling".
But what's the best way to do this? Can I like replace the touchesMove of the variable tableView? I could create an extension of the UITableView and override the function, but then I don't have access to the view of my Controller to move the tableView.
Any answer? Thanks!
The imageView should be behind the tableView with constraints top, leading, trailing to superview and height to superview with a multiplier of 0.5. The tableView should fill its superview.
The trick is that you add a tableViewHeader that is invisible and equal to half the height of the screen. This has the effect of pushing the initial content of the tableView off the screen. In interface builder add a UIView to the tableView as header and make it transparent. Also make the background of your tableView transparent. Take an outlet to the headerView and the tableView. In viewDidLayoutSubviews set your headerView.frame.size.height = tableView.frame.size.height / 2.
Apart from what Josh answered, what you can try is:
What you can do is make the UIImageView's height decrease based on the scroll amount until the UIImageView's height is 0 and then start the scrolling otherwise force the contentOffSet of the UITableView to always remain 0. That being said, here is how to do it:
Make an enum to keep track of the various states of the UIImageView like:
enum Layout {
case ImageExpanding,
case ImageDefaultHeight,
case ImageDiminishing,
case ImageNotVisible
}
In your storyboard, add a top, leading and trailing constraint to the superview for your UIImageView and fixed height constraint of lets say 200(don't worry you will change this later). To your UITableView add a leading, trailing and bottom constraint to the superview and a top the UIImageView. Now drag and drop a constraint outlet for the UIImageView height into your UIViewController.
In your viewDidLoad set your heightConstraint to be 1/2 of the total screen height and set the enum state to initially be ImageDefaultHeight
Now in your scrollViewDidScroll you will have to check the direction of the scroll and based on that while checking the current state of the image, increase or decrease the heightConstraint based on the amount a user scrolls by and always check:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(/*up*/){
//just telling you about this if condition as this will ensure that
//if your table view is large and if tableview isn't at the top,
//the image shouldn't expand
if layout == .ImageNotVisible{
if scrollView.contentOffset.y <= 0 {
//start expanding once you reach top of tableview
layout = .ImageExpanding;
}
}
//keep track of other enum states and update your uiimageview
//appropriately after calculating the user scroll amount
//until the height reaches your initialDefaultHeight and
//then do nothing. You will have to figure out the code
//for this on your own
}else if(/*down*/){
//make image smaller
}
//dont let table view scroll until image height is 0 when use is scrolling down
if layout != ImageNotVisible{
scrollView.contentOffset = CGPoint(x: 0, y: 0)
}
}
I am having abit of trouble here trying to make this post description label to grow and shrink based on content size on IOS9. I have a view (I will refer it topView) that I am using as a header for the tableview (So when I scroll up the header disappears). Inside the topView, there are a bunch of stack views. I wish to grow and shrink the post description label in height based on content size. I do know how to do it in simple case where everything is inside the prototype cell (i.e. set estimated row height and set uitableviewautomaticDimensions, set sizetoFit on label and change number of lines to 0). However, this is a different case because the post description label is not really inside the cell, it is in its view before the table view cells.
Note that all items in the view has static height except the postdescription label. Post description label is inside a stack view that is pinned only left and right (So that top and bottom would grow?). Also, the main stack view that contains all elements is pinned towards the four sides with the topview that contains the main stack view also pinned towards the four sides. With this setup, I would expect the topview to grow and shrink based on the content size. However, I do not see that in the output. I dont know if it is the stackview that is holding the label refusing to grow or the top view refusing to grow to allow more space for the stackview for the label. Thanks
UPDATE
Thanks Riadluke, I tried doing something as suggested which is resizing the headerview after calculating the required height. I have placed the following code in viewDidLayoutSubview and it works with an issue
postDescriptionLbl.sizeToFit()
let headerView = commentTableView.tableHeaderView!
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = TopStackView.frame.size.height + ImageStackView.frame.size.height + postDescriptionLbl.frame.size.height + SpacerStackView.frame.size.height + BottomStackView.frame.size.height
headerView.frame.size.height = height
commentTableView.tableHeaderView = headerView
The issue I now have with this method is that when the view controller appears, I can physically see the postDescription label height grow from the default height in storyboard to the required height. For example, when the VC first appears, I see a line of label with some string being cut off, however after 0.5 second, the headerview and the label grow to the size that I wanted. I know this would be expected because I was calling the manipulation after the viewDidLayout Subview. I was wondering if there is a better way such that I dont see that transition and the view appears to be the right height straight away. Ie. let the view know exactly how high the label is to determine how high the headerview needs to be before appearing on screen?
I'm afraid the view set as tableHeaderView of a UITableView does not get resized automatically. Its height will be fixed to the height it had in IB.
What you have to do is set its size manually and then reassign it as the tableHeaderView so it is displayed in the height you want.
It could take only few lines since you're using autolayout.
You can try this code right after you've set the header view's contents:
//for the target size you have set the width as your tableView's width when it is already displayed on screen.
//note that when it is accesed inside viewDidLoad the tableView's bounds
//may be different to the actual bounds it will be displayed with,
//here I am just using the screen bounds
let targetSize = CGSize(width: UIScreen.mainScreen().bounds.width, height: 10000)
//set the tableHeader's size to its size after its layout constraints are resolved
tableHeader.bounds.size = tableHeader.systemLayoutSizeFittingSize(targetSize)
//reassign it as the tableHeaderView to update the height it will be displayed in
tableView.tableHeaderView = tableHeader
After many many attempts, I could not get anything to work with the original setup. The best I achieved was to resize it after view did appear which is not idea as you see the previous layout.
It is now working with a complete different approach. I have created two prototype cell and have one as "HeaderViewCell" and implemented the following functions
commentTableView.estimatedSectionHeaderHeight = 400
commentTableView.sectionHeaderHeight = UITableViewAutomaticDimension
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
Everything works like a charm after that.