iOS UIView scrolling UI - how to implement? - ios

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];
}

Related

How to set map view parallax effect like Citymapper app? [duplicate]

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
}
}

programmatically scroll to a specific page/position with autolayout iOS

I am using autolayout with a scroll view. It is working great, but I need to be able to scroll to specific pages programmatically. Specifically, I need to scroll to the last page after the view loads. How do I do this? My scrolling essentially has the visual format shown below, except the number of pages is dynamic.
visual format for vertically scrolling (swap V and H for horizontally scrolling):
view hierarchy is scrollContainer(UIView) -> scrollView(UIScrollView) -> contentView(UIView) -> pages(UIView’s)
V:|scrollView|
H:|scrollView|
V:|contentView|
H:|contentView|
V:|[page1(==scrollContainer)][page2(==scrollContainer)][page3(==scrollContainer)]|
H:|[page1(==scrollContainer)]|
H:|[page2(==scrollContainer)]|
H:|[page3(==scrollContainer)]|
edit: clarifying my question
The code to offset the scrollview is below. But when can I get the origin of the page I want to scroll to using autolayout? If I put it in viewDidLayoutSubviews, it updates every time the device is rotated. I need to update just when the view is loaded. Is there a more appropriate method to override? Or is there a way of setting the origin with a constraint?
let lastPage = pages?.last
if lastPage != nil {
let origin = lastPage!.frame.origin
self.scrollView.setContentOffset(origin, animated: false)
}
If it's a horizontal scroll view:
CGPoint lastPage = CGPointMake((numberOfPages - 1) * pageWidth, 0.0f);
If vertical:
CGPoint lastPage = CGPointMake(0.0f, (numberOfPages - 1) * pageHeight);
Then just use the scroll view's method:
[scrollView setContentOffset:lastPage animated:YES];
You can leave this code in viewDidLayoutSubviews, just add an if-statement to test if it's the first time or not:
if (!self.alreadyScrolled) {
[self scrollToDesiredPage];
self.alreadyScrolled = YES;
}
You should use:
CGPoint desiredOffset = CGPointMake(0.0,desiredPageNumber * pageHeight);
[self.scrollView setContentOffset:desiredOffset animated:YES];
Obviously tweak it as you need to. I don't believe you can or should achieve this by animating constraints, because of the unique way autolayout treats scroll views.

IOS - Keep UIView on top of UITableView on scroll

I have a UITableView with a UIView on top. I want the UIView to stick to the top as the tableView cells scroll over it.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (self.tableView.contentOffset.y > 0) {
CGRect newframe = self.publicTopView.frame;
newframe.origin.y = -self.tableView.contentOffset.y;
self.publicTopView.frame = newframe;
NSLog(#"After: %f", self.publicTopView.frame.origin.y);
}
}
You need to set your table view header view to the view you want on top.
Add this code to you viewDidLoad
self.tableView.tableHeaderView = self.publicTopView
I'm not certain what you're trying to accomplish, but I have a guess at what is wrong. As you scroll your contentOffset will continue to change and let's say your tableView has a content size height of 1500, then your contentOffset will eventually be larger than the height of your view controllers view. Now see that you are putting that contentOffset into the origin.y of your publicTopView. So your publicTopView could possibly be moving too much, even offscreen depending on how large your tableview's content size is.

iOS: Stretching / Resizing UITableView Header As The User Drags Down?

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
}
}

Nested UIScrollViews and event routing

I have 2 scroll views, both of which are supposed to scroll vertically. The outer scroll view (red) contains a search bar and the inner scroll view (blue). The inner scroll view is supposed to scroll infinitely (it contains images/items and has an endless scrolling implementation).
The way I want this controller to work is as follows:
When I scroll down, the outer scroll view should scroll first and the search bar should disappear (scroll out of the content area). Only after that the inner scroll view should start scrolling.
When scrolling back up, the inner scroll view should scroll all the way to its top. Only then the outer scroll view should take the scroll events and finally scroll up to make the search bar visible again.
If I just nest them in IB without any modifications the inner scroll view catches all the scroll events and it works the other way around.
Please bear in mind that I'm using the inner scroll view as a simplifying metaphor here. In my app I actually have a control here, which has a scroll view with nested table views (scroll view lets me page horizontally, table views let me scroll vertically).
If you are able to, set a common UIScrollViewDelegate on the 2 scroll views, and implement the following:
- (void) scrollViewDidScroll: (UIScrollView*) scrollView
{
if (scrollView == self.mainScrollView)
{
/* Handle instances when the main scroll view is already at the bottom */
if ( scrollView.contentOffset.y
== scrollView.contentSize.height - scrollView.bounds.size.height)
{
/* Stop scrolling the main scroll view and start scrolling the
* inner scroll view
*/
self.innerScrollView.scrollEnabled = YES;
self.mainScrollView.scrollEnabled = NO;
}
else
{
/* Start scrolling the main scroll view and stop scrolling the
* inner scroll view
*/
self.innerScrollView.scrollEnabled = NO;
self.mainScrollView.scrollEnabled = YES;
}
}
else if (scrollView == self.innerScrollView)
{
/* Handle instances when the inner scroll view is already at the top */
if (self.innerScrollView.contentOffset.y == 0)
{
/* Stop scrolling the inner scroll view and start scrolling the
* main scroll view
*/
self.innerScrollView.scrollEnabled = NO;
self.mainScrollView.scrollEnabled = YES;
}
else
{
/* Start scrolling the inner scroll view and stop scrolling the
* main scroll view
*/
self.innerScrollView.scrollEnabled = YES;
self.mainScrollView.scrollEnabled = NO;
}
}
}
Please note that I haven't tested this, but the logic could be somewhat like this (either you set scrolling enabled or you disable user interaction, or something). Most probably this will not be enough of a solution as you would like, but I'm sure a common UIScrollViewDelegate is the solution to your problem.
I given the example for one scrollview, samething you have to create onemore scrollview and add based on dynamic height and content size it will work.
// .h file
#property (nonatomic, strong) UIScrollView *scrlSearch;
// .m File // ViewDidLoad
scrlSearch = [[UIScrollView alloc] init];
// For first scroll screen height was ((total screen height / 100 )* 10% )
// For Second scroll screen height was ((total screen height / 100 )* 90% )
scrlSearch.frame = CGRectMake(0, 0, (YourScreenWidth), (YourScreenHeight));
// YourVIEW = add any view to scrollbar
[scrlSearch addSubview:YourVIEW];
CGSize contentSize = scrlSearch.frame.size;
// YourContentHeight = dynamic content or static content height
contentSize.height = YourContentHeight;
// set the ContentHeight for scrolling
[scrlSearch setContentSize:contentSize];
// add the scrollview into delegate
[scrlSearch setDelegate:self];
Use just one scrollView and set the contentInset/contentOffset for the search bar. Something along this line:
UIEdgeInsets oldEdgeInset = [[self scrollView] contentInset];
CGRect f = [[self searchBar] frame];
UIEdgeInsets newEdgeInset = UIEdgeInsetsMake(CGRectGetMaxY(f), 0, 0, 0);
CGPoint offset = [[self scrollView] contentOffset];
offset.y += oldEdgeInset.top - newEdgeInset.top;
[[self scrollView] setContentOffset:offset];
[[self scrollView] setContentInset:newEdgeInset];
[[self searchBar] setFrame:f];
I am not sure about the structure you have and want to implement..
I have made a test project find here
But the project will definitely help you to manage different scrollviews all together..
The application might not be perfect though, but will give you some idea to achieve the solution.
Hope it helps..
Cheers
You should add inner scrollview on outer scrollview, but inner scrollview 'y' position should be content off set of outer scrollview like...
innerScrollView.frame = CGRectMake(10, outerScrollView.contentSize.height, 300, 440);
After this you can add any view on this innerScrollView, you can set contentOffset etc for innerScrollView.
Finally you should increase the contentSize for outer scrollview.
outerScrollView.contentSize = CGSizeMake(320, innerScrollView.frame.size.height+actualContentSizeOfOuterScroolView);
I think it helps for you!
Create a swipe gesture recognizer to your top-most view (above all the scroll views, on your view controller's view), and make it recognize UISwipeGestureRecognizerDirectionUp.
Then, you need to notify your controller whenever the outer scroll view scrolls.
Once it's scrolled down, add the gesture recognizer. When it hits the top again (outerScrollView.contentOffset == (0,0)), remove the gesture recognizer.
The gesture should 'eat' all the swiping events while it's present, making your inner scroll views to not receive the touch event and therefore will not scroll

Resources