I have a new design for my app and it include a parallax scroll for a image display on top of tableView.
I know how to add a parallax effect with putting a image in to a cell, like this -
when table scrollViewDidScroll get called :
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint currentOffset = scrollView.contentOffset;
if (currentOffset.y > _lastContentOffset.y) {
//Scroll Up
_containerView.clipsToBounds = true;
_bottomSpaceConstraint.constant = -scrollView.contentOffset.y / 2;
_topSpaceConstraint.constant = scrollView.contentOffset.y / 2;
} else {
//Scroll Down
_topSpaceConstraint.constant = scrollView.contentOffset.y;
_containerView.clipsToBounds = false;
}
_lastContentOffset = currentOffset;
}
(the _bottomSpaceConstraint and _topSpaceConstraint are the Image top and Bottom constraint which inside the tableView cell in section 0)
But my problem is I needs to stop the image scrolling up when it reached the size of my navigation bar. (my navigation bar background is transparent) I don't want the image to go all the way top. But it is a cell which is inside my tableView so it is going all the way to top until it disappear when scrolling. I need help for stop this image get scrolled. Maybe my approach is not correct if I want to achieve this effect.
This effect is on android and its called "collapsing toolbar layout".
http://antonioleiva.com/collapsing-toolbar-layout/
Does someone know how to do this "collapsing toolbar layout" effect for iOS.
Thanks a lot for any help!
Try creating a UIView that will contain the image and set the tableHeaderView in the tableView. Then in scrollViewDidScroll: update the height and top constraints of the header view.
Related
I know that the sticky header is not a new thing to ask advice on, but still...
I am trying to create a sticky header (UIImageView) and the scrolling part (UIScrollView with a UIStackView in it)
Im using the scrollViewDidScroll method from the UIScrolLViewDelegate. The only problem is, that when I scroll the view up, I am not only decreasing the height of the header view, but also scrolling the content of the stack view. So when I scroll further up, you still can see the header view, but the top content of the scroll view disappears by scrolling.
Can this be solved somehow that when I scroll up, the content of the stack view is scrolling up and not also disappearing? And starts disappearing when the header view disappears?
Thank you
The easiest way to do this is to use a table view. Your sticky header is the table's first section header, while the scrolling part is the second section.
If you can't use a table, for whatever reason, then you have to mess around with the scroll view's content offset. When the contentOffset.y is growing (but not beyond your header's height), reset it to 0 and decrease the header's height accordingly. After the header's height is 0, stop messing with contentOffset - until you come back and the contentOffset.y wants to go into negatives.
P.S. the second solution requires you to enable bouncing on the scroll view. Otherwise, the header will be hidden, but won't show again (unless you reset the controller)
L.E. Some (old) code for my second solution:
let headerHeight = self.headerView.height
self.scrollHandler = {
var offset = self.detailsTable.contentOffset
self.headerTopConstraint.constant -= offset.y
if self.headerTopConstraint.constant < -headerHeight {
self.headerTopConstraint.constant = -headerHeight
let size = self.detailsTable.contentSize
if offset.y + self.detailsTable.frame.height > size.height {
offset.y = size.height - self.detailsTable.frame.height
}
self.detailsTable.contentOffset = offset
}
else {
if self.headerTopConstraint.constant > 0 {
self.headerTopConstraint.constant = 0
}
self.detailsTable.contentOffset = CGPointZero
}
}
Please note that my code moved the header upwards to hide it. As far as I understood, you just have to change the header's height (by the same amount I move it upwards).
I have a UIScrollView A (in fact a UICollectionView) filling the screen inside a UINavigationController B. The controller B's adjustScrollViewInsets is set to true.
I want to hide the navigation bar when user scrolls up, and show it when down. Following is my code:
func scrollViewDidScroll(scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentSize.height - scrollView.frame.size.height && self.lastContentOffset > scrollView.contentOffset.y) {
// dragging down
if self.navigationController!.navigationBarHidden {
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
} else if (self.lastContentOffset > 0 && self.lastContentOffset < scrollView.contentOffset.y) {
// dragging up
if !self.navigationController!.navigationBarHidden {
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
self.lastContentOffset = scrollView.contentOffset.y
}
Now the problem is, since the screen of iPhone 6+ is too large, the contentSize of the scroll view A is smaller than its frame(i.e. the full screen frame) when the navigation bar is hidden. In such circumstance, the scroll view will not be scrollable, and the navigation bar will never be back again.
I want to manually maintain the height of the contentSize of A to screen at least height + 1, but don't know how to do this. Could anyone help? Or provide a better solution?
BTW, I am using iOS 8 and Swift.
Lets say you need to keep the minimum content size of scroll view to 100(of course this will be dynamic and vary according to device)
NSInteger minScrollViewContentHeight = 100;
After populating the scroll view with content, you need to check if the scroll view's content size is less than minimum required scroll views content size. If its lesser than the required content size than you need to set the minimum content size of the scroll view as follows -
if(scrollView.contentSize.height < minScrollViewContentHeight)
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, minScrollViewContentHeight)];
Off the top of my head (I'm on a phone), contentSize is not read-only I think.
How about changing it manually to the desired amount depending on the circumstances of scrolling direction etc?
Something like:
IF navbar is hidden THEN contentSize = whatever
An option would be to use the appearance and disappearance of cells to trigger the show/hide.
Use the delegate methods collectionView:willDisplayCell:forItemAtIndexPath: and collectionView:didEndDisplayingCell:forItemAtIndexPath: to detect movement. You can work out the direction from the index change of the cells being shown or removed. If you cannot scroll off screen then nothing happens.
You have to change no offset (which is actually just scrolling position), but contentSize itself. That means, that when you hide navigation bar, increase contentSize by navigation height (don't remember numbers) and when you show navigation bar, decrease contentSize. Or... Use AutoLayout and layoutIfNeeded method after showing/hiding navigation bar.
I stumbled upon a similar problem. I needed a minimum scrollable area for a tableview i was using.
ScrollView might be a bit easier since you can directly modify the contentView size.
If you're using autoLayout, try adding equal heights constraint between the contentView and the scrollView itself. Something along the lines of contentView.height = scrollView.height + scrollMin;
For my tableView i had to subclass UITableView and override the contentSize setter.
#define MIN_SCROLL 60
- (void)setContentSize:(CGSize)contentSize {
//take insets into account
UIEdgeInsets insets = self.contentInset;
CGFloat minHeight = self.frame.size.height - insets.top - insets.bottom + MIN_SCROLL;
if(contentSize.height < minHeight) {
contentSize.height = minHeight;
}
[super setContentSize:contentSize];
}
There is a button at the bottom of my view controller. When the user scrolls down the button has to be attached to the scrollview at certain height.
I need to attach a button to the scrollview, immediately when the contentOffset.y reaches a particular value. -(void) scrollviewDidScroll doesn't help me as there might be a jump in contentOffset when the user is scrolling fast. Any leads on this are helpful.
Also, whenever I add a subview to the scrollview, -(void) viewDidLayoutSubviews is called. Which in turn sets the contentOffset to {0,0}. How can I achieve the functionality I need?
I needed to do the same thing with a UITableView and for me using scrollViewDidScroll worked.
I created a view called staticBar and added it as a subview of the tableView, but I had to rearrange the tableview subviews for it to appear in the right place. I don't have my code in front of me, but in -scrollViewDidScroll: it looked something like this:
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
CGFloat staticBarAdjustedY = _staticBarY - scrollView.contentOffset.y;
CGFloat scrollViewYFloor = scrollView.frame.size.height - _staticBar.frame.size.height;
// This way maximum Y the view can have is at the base of the scrollView
CGFloat newY = MIN( staticBarAdjustedY, scrollViewYFloor);
_staticBar.frame = (CGRect){ { _staticBar.frame.origin.x, newY}, _staticBar.frame.size}
}
I will check my code later today and add more details here.
Also, you said the scrollviewDidScroll has jumps in contentOffset, but it's worth mentioning that these jumps are the same that the scrollView uses to scroll its own view. So it's not like you are "losing" frames on this delegate method.
Hope it helps.
PS: So, here is the rest of my code.
//I place my custom view as a subview of the tableView below it's last subview
//The last subview is for scroll indicators.
WTButtonsBar *buttonBar = [[WTButtonsBar alloc] init];
[self.tableView insertSubview:buttonBar belowSubview:self.tableView.subviews.lastObject];
In scrollViewDidScroll:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//In my app I needed my view to stick to the top of the screen
//thats why I use MAX here
//self.buttonsBarOriginalY is the view's position in the scrollView when it isn't attached to the top.
CGFloat newY = MAX(scrollView.contentOffset.y, self.buttonsBarOriginalY)
[_buttonsBar setFrame:(CGRect){{0, newY}, _buttonsBar.frame.size}];
}
Using storyboard, I have placed an imageView as my tableView's headerView inside a ViewController.
This is how my storyboard is set up:
Depending on what data the user is viewing, the viewController will either show or hide the headerView. My question is, that when the headerView is visible and the user drags down on the tableView, how can I have the imageView stick to both the navigationBar and the tableView as it resizes to cover the space in between?
This is what it currently does:
But this is what I'm going for:
Any help would be greatly appreciated. I've looked at parallax libraries, but none support sectionTitles, and I'm not necessarily going for the parallax effect either. When the user scrolls up, I want it to bounce back to the regularView and not hide the headerView. Thanks!
UPDATE:
I have followed the advice posted by Dany below and have done the
following:
-(void)scrollViewDidScroll:(UIScrollView*)scrollView {
CGRect initialFrame = CGRectMake(0, 0, 320, 160);
if (scrollView.contentOffset.y < 0) {
initialFrame.size.height =! scrollView.contentOffset.y;
childHeaderView.frame = initialFrame;
} }
childHeaderView is an imageView and for some reason when I drag down,
the image moves up (like half of it behind the navBar) and doesn't return. Any advice would be
greatly appreciated!! Thanks!
I recently posted a blog post about accomplishing this using constraints which might help, turns out it was quite straight forward.
Here is the link: Creating parallax effect on UIScrollView using constraints
First of all you should remove the UIImageView from the header and add it as a simple UIImageView on top of the UITableView then since UITableViewDelegate protocol conforms to UIScrollViewDelegate protocol you can implement the scrollViewDidScroll: method to check when the tableView is scrolling down and has a bouncing effect. something like this:
-(void)someInitMethod {
initialFrame = yourHeaderView.frame;
}
-(void)scrollViewDidScroll:(UIScrollView*)scrollView {
if(scrollView.contentOffset.y < 0) {
initialFrame.size.height -= scrollView.contentOffset.y;
yourHeaderView.frame = initialFrame;
}
}
Also make sure you set the proper contentMode for your UIImageView. Also I think this implementation will create a bouncing effect but I'm not sure because I can't test it right now but I think this is a good start point for you.
This is how I achieved it, in my case I was using a map view up the top:
Create a View Controller in storyboard.
Add a Table View and set the constraints to 0 from all sides.
Add a Map View (or whatever view) below the Table View so that it will get rendered over the top. It will look like it is overlapping.
Add constraints to the top left and right.
In the view controller viewDidLoad add the following: tableView.contentInset = UIEdgeInsetsMake(200, 0, 0, 0) where 200 is the height of the View. This will push the contents of the table view downwards.
In the view controller add the following code, which resizes the view based on the scrolling:
func scrollViewDidScroll(scrollView: UIScrollView) {
var scrollOffset = scrollView.contentOffset.y
var headerFrame = mapView.frame
if (scrollOffset < 0) {
// Adjust map
headerFrame = CGRect(x: mapView.frame.origin.x,
y: mapView.frame.origin.y,
width: mapView.frame.size.width,
height: -scrollOffset)
} else {
// Adjust map
headerFrame = CGRect(x: mapView.frame.origin.x,
y: mapView.frame.origin.y,
width: mapView.frame.size.width,
height: 0)
}
mapView.frame = headerFrame
}
If I could set contentInset from the storyboard it would be even more pretty
Please have a look at this https://github.com/matteogobbi/MGSpotyViewController which implements the same effect as per your requirement.
The earlier solutions on this page gave me some trouble when I needed this to work along with section titles and index bar, so I came up with the following alternative myself. Please note; I don't use autolayout in my project and I've only tested this on iOS9+;
In your project's storyboard:
Create a UITableView within a UIViewController (or try it with a UITableViewController).
Drop a UIView at the top (but within) the UITableView, so it becomes a table header above the first cell.
Give this header view a desired height (like 200px for example) and set the background color to "Clear Color". The Clear Color is important, the view needs to be see-through.
Drop a 2nd UIView within the table header UIView and make it the same size as it's parent. This will be the actual header, so feel free to give it any color, setup an image view or other content.
Connect this 2nd UIView to your UIViewController IBOutlet, I named it "headerView" in my case.
Next, go to your UIViewController.m:
- (void)viewDidLoad
{
[super viewDidLoad];
// Remove view from table header and place it in the background instead.
[self.headerView removeFromSuperview];
UIView *backgroundView = [UIView new];
[backgroundView addSubview:self.headerView];
self.tableView.backgroundView = backgroundView;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
/* Set initialScrollOffset ivar to start offset, because in my case
the scroll offset was affected by the statusbar + navigation bar heights
and the view controller's "extend edges under top bars" option. */
initialScrollOffset = self.tableView.contentOffset.y;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
/* Modify headerView height only if the table content gets pulled
beyond initial offset. */
if (scrollView.contentOffset.y < initialScrollOffset) {
CGRect frame = self.headerView.frame;
frame.size.height = self.tableView.tableHeaderView.frame.size.height + -scrollView.contentOffset.y;
self.headerView.frame = frame;
}
}
I needed this implementation only for a stretching header with background color and labels. It should be easy to add a UIImageView to this header though.
Also, steps 1 to 5 are completely optional of course. You can programmatically create your header view or use a XIB instead. As long as you make sure the table has a Clear Colored header view set with the same height as your desired header because this serves as a spacer to keep your cells and section titles in line.
EDIT:
I found an even cleaner way to accomplish this;
Build up your table header in interface builder as described above: 1 UIView as container with a 2nd UIView embedded within.
Skip the viewDidLoad code above, there is no need to pull the UIView out of it's container and we won't need to set it as a table background.
Change the scrollViewDidScroll: method to this:
UIViewController.m:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y < initialScrollOffset) {
CGRect frame = self.headerView.frame;
frame.size.height = self.tableView.tableHeaderView.frame.size.height - scrollView.contentOffset.y;
frame.origin.y = self.tableView.tableHeaderView.frame.origin.y + scrollView.contentOffset.y;
self.headerView.frame = frame;
}
}
That's it. Only visual difference from the other solution is that the contents will now scroll up along with the rest of the cells instead of being overlapped by them.
I don't know, if this would help you or not ..
Set your scroll delegate to self.
and then implement this:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
float scrollViewHeight = scrollView.frame.size.height;
float scrollContentSizeHeight = scrollView.contentSize.height;
float scrollOffset = scrollView.contentOffset.y;
if (scrollOffset == 0)
{
// then we are at the top
}
else if (scrollOffset + scrollViewHeight == scrollContentSizeHeight)
{
// then we are at the end
// Do what you need here
}
}
Forgive me to the obtuse title, as I'm unsure how to describe this question.
Recently many iOS apps utilise a scrolling UI design pattern which helps to maximise screen real-estate, typically hiding the header when the user scrolls downwards.
For example, Instragram's main view has the Instragram header at the top. Scrolling upwards on this view keeps the header fixed at the top, and the view bounces back normally to the top of the content. But scroll down and the header acts as part of the content, making way for an extra 44 points of vertical space.
Its probably that I haven't done much iOS work in a while, but I can't easily figure out how best to impliment this? Apologies for the terrible description.
If the header stays put no matter what, use a separate view on top of the scroll view.
If you use UITableView, you can use section headers.
EDIT Use this code:
- (void)scrollViewDidScroll:(UIScrollView*) scrollView
{
CGPoint offset = scrollView.contentOffset;
CGRect headerFrame = _headerView.frame;
if(offset.y > 0){
headerFrame.origin.y = offset.y;
}
else{
headerFrame.origin.y = 0.0;
}
[_headerView setFrame:headerFrame];
}
(Assumes _headerView is your header, sitting on top of the scroll view, not inside it. Also, both scroll view and header begin at the top of their parent view, y==0. Also, your view controller must be set up as delegate of the scroll view)
I just wrote this code from memory; haven't tested it but at most it should only need tweaking.
I tried ranReloaded's answer above but it seems that calling setFrame: on a UIScrollView stops the view from bouncing when going beyond its bounds.
Instead I set the scroll view to fit inside another UIView called scrollerWrapper. Applying the calculated origin and height to this view gives me effect I'm after plus retains the bounce behaviour.
- (void)scrollViewDidScroll:(UIScrollView*) scrollView
{
CGPoint offset = scrollView.contentOffset;
CGRect headerFrame = header.frame;
CGRect wrapperFrame = scrollerWrapper.frame;
if(offset.y > 0){
headerFrame.origin.y = -offset.y;
wrapperFrame.origin.y = MAX(0, headerFrame.size.height - offset.y);
}
else{
headerFrame.origin.y = 0.0;
wrapperFrame.origin.y = headerFrame.size.height;
}
wrapperFrame.size.height = self.view.frame.size.height - wrapperFrame.origin.y;
[header setFrame:headerFrame];
[scrollerWrapper setFrame:wrapperFrame];
}