In the new iOS7 Facebook iPhone app, when the user scrolls up the navigationBar gradually hides itself to a point where it completely vanishes. Then when the user scrolls down the navigationBar gradually shows itself.
How would you implement this behavior yourself? I am aware of the following solution but it disappears right away and it isn't tied to the speed of the user's scroll gesture at all.
[navigationController setNavigationBarHidden: YES animated:YES];
I hope this isn't a duplicate as I'm not sure how best to describe the "expanding/contracting" behavior.
The solution given by #peerless is a great start, but it only kicks off an animation whenever dragging begins, without considering the speed of the scroll. This results in a choppier experience than you get in the Facebook app. To match Facebook's behavior, we need to:
hide/show the navbar at a rate that is proportional to the rate of the drag
kick off an animation to completely hide the bar if scrolling stops when the bar is partially hidden
fade the navbar's items as the bar shrinks.
First, you'll need the following property:
#property (nonatomic) CGFloat previousScrollViewYOffset;
And here are the UIScrollViewDelegate methods:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect frame = self.navigationController.navigationBar.frame;
CGFloat size = frame.size.height - 21;
CGFloat framePercentageHidden = ((20 - frame.origin.y) / (frame.size.height - 1));
CGFloat scrollOffset = scrollView.contentOffset.y;
CGFloat scrollDiff = scrollOffset - self.previousScrollViewYOffset;
CGFloat scrollHeight = scrollView.frame.size.height;
CGFloat scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom;
if (scrollOffset <= -scrollView.contentInset.top) {
frame.origin.y = 20;
} else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
frame.origin.y = -size;
} else {
frame.origin.y = MIN(20, MAX(-size, frame.origin.y - scrollDiff));
}
[self.navigationController.navigationBar setFrame:frame];
[self updateBarButtonItems:(1 - framePercentageHidden)];
self.previousScrollViewYOffset = scrollOffset;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self stoppedScrolling];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate
{
if (!decelerate) {
[self stoppedScrolling];
}
}
You'll also need these helper methods:
- (void)stoppedScrolling
{
CGRect frame = self.navigationController.navigationBar.frame;
if (frame.origin.y < 20) {
[self animateNavBarTo:-(frame.size.height - 21)];
}
}
- (void)updateBarButtonItems:(CGFloat)alpha
{
[self.navigationItem.leftBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* item, NSUInteger i, BOOL *stop) {
item.customView.alpha = alpha;
}];
[self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* item, NSUInteger i, BOOL *stop) {
item.customView.alpha = alpha;
}];
self.navigationItem.titleView.alpha = alpha;
self.navigationController.navigationBar.tintColor = [self.navigationController.navigationBar.tintColor colorWithAlphaComponent:alpha];
}
- (void)animateNavBarTo:(CGFloat)y
{
[UIView animateWithDuration:0.2 animations:^{
CGRect frame = self.navigationController.navigationBar.frame;
CGFloat alpha = (frame.origin.y >= y ? 0 : 1);
frame.origin.y = y;
[self.navigationController.navigationBar setFrame:frame];
[self updateBarButtonItems:alpha];
}];
}
For a slightly different behavior, replace the line that re-positions the bar when scrolling (the else block in scrollViewDidScroll) with this one:
frame.origin.y = MIN(20,
MAX(-size, frame.origin.y -
(frame.size.height * (scrollDiff / scrollHeight))));
This positions the bar based on the last scroll percentage, instead of an absolute amount, which results in a slower fade. The original behavior is more Facebook-like, but I like this one, too.
Note: This solution is iOS 7+ only. Be sure to add the necessary checks if you're supporting older versions of iOS.
EDIT: Only for iOS 8 and above.
You can try use
self.navigationController.hidesBarsOnSwipe = YES;
Works for me.
If your coding in swift you have to use this way (from https://stackoverflow.com/a/27662702/2283308)
navigationController?.hidesBarsOnSwipe = true
Here is one more implementation: TLYShyNavBar v1.0.0 released!
I decided to make my own after trying the solutions provided, and to me, they were either performing poorly, had a a high barrier of entry and boiler plate code, or lacked the extension view beneath the navbar. To use this component, all you have to do is:
self.shyNavBarManager.scrollView = self.scrollView;
Oh, and it is battle tested in our own app.
You can have a look at my GTScrollNavigationBar. I have subclassed UINavigationBar to make it scroll based on the scrolling of a UIScrollView.
Note: If you have an OPAQUE navigation bar, the scrollview must EXPAND as the navigation bar gets HIDDEN. This is exactly what GTScrollNavigationBar does. (Just as in for example Safari on iOS.)
iOS8 includes properties to get the navigation bar hiding for free. There is a WWDC video that demonstrates it, search for "View Controller Advancements in iOS 8".
Example:
class QuotesTableViewController: UITableViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationController?.hidesBarsOnSwipe = true
}
}
Other properties:
class UINavigationController : UIViewController {
//... truncated
/// When the keyboard appears, the navigation controller's navigationBar toolbar will be hidden. The bars will remain hidden when the keyboard dismisses, but a tap in the content area will show them.
#availability(iOS, introduced=8.0)
var hidesBarsWhenKeyboardAppears: Bool
/// When the user swipes, the navigation controller's navigationBar & toolbar will be hidden (on a swipe up) or shown (on a swipe down). The toolbar only participates if it has items.
#availability(iOS, introduced=8.0)
var hidesBarsOnSwipe: Bool
/// The gesture recognizer that triggers if the bars will hide or show due to a swipe. Do not change the delegate or attempt to replace this gesture by overriding this method.
#availability(iOS, introduced=8.0)
var barHideOnSwipeGestureRecognizer: UIPanGestureRecognizer { get }
/// When the UINavigationController's vertical size class is compact, hide the UINavigationBar and UIToolbar. Unhandled taps in the regions that would normally be occupied by these bars will reveal the bars.
#availability(iOS, introduced=8.0)
var hidesBarsWhenVerticallyCompact: Bool
/// When the user taps, the navigation controller's navigationBar & toolbar will be hidden or shown, depending on the hidden state of the navigationBar. The toolbar will only be shown if it has items to display.
#availability(iOS, introduced=8.0)
var hidesBarsOnTap: Bool
/// The gesture recognizer used to recognize if the bars will hide or show due to a tap in content. Do not change the delegate or attempt to replace this gesture by overriding this method.
#availability(iOS, introduced=8.0)
unowned(unsafe) var barHideOnTapGestureRecognizer: UITapGestureRecognizer { get }
}
Found via http://natashatherobot.com/navigation-bar-interactions-ios8/
I have some kind of a quick and dirty solution for that. Haven't made any in-depth testing but here's the idea:
That property will keep all the items in the navbar for my UITableViewController class
#property (strong, nonatomic) NSArray *navBarItems;
In the same UITableViewController class I have:
-(void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
if([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0f){
return;
}
CGRect frame = self.navigationController.navigationBar.frame;
frame.origin.y = 20;
if(self.navBarItems.count > 0){
[self.navigationController.navigationBar setItems:self.navBarItems];
}
[self.navigationController.navigationBar setFrame:frame];
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0f){
return;
}
CGRect frame = self.navigationController.navigationBar.frame;
CGFloat size = frame.size.height - 21;
if([scrollView.panGestureRecognizer translationInView:self.view].y < 0)
{
frame.origin.y = -size;
if(self.navigationController.navigationBar.items.count > 0){
self.navBarItems = [self.navigationController.navigationBar.items copy];
[self.navigationController.navigationBar setItems:nil];
}
}
else if([scrollView.panGestureRecognizer translationInView:self.view].y > 0)
{
frame.origin.y = 20;
if(self.navBarItems.count > 0){
[self.navigationController.navigationBar setItems:self.navBarItems];
}
}
[UIView beginAnimations:#"toggleNavBar" context:nil];
[UIView setAnimationDuration:0.2];
[self.navigationController.navigationBar setFrame:frame];
[UIView commitAnimations];
}
That's only for ios >= 7, it's ugly I know but a quick way to achieve this. Any comments/suggestions are welcome :)
This works for iOS 8 and above and ensures that the status bar still retains its background
self.navigationController.hidesBarsOnSwipe = YES;
CGRect statuBarFrame = [UIApplication sharedApplication].statusBarFrame;
UIView *statusbarBg = [[UIView alloc] initWithFrame:statuBarFrame];
statusbarBg.backgroundColor = [UIColor blackColor];
[self.navigationController.view addSubview:statusbarBg];
And if you want to show the nav bar when you tap on the status bar you can do this:
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
self.navigationController.navigationBarHidden = NO;
}
Here is my implementation: SherginScrollableNavigationBar.
In my approach I am using KVO for observing UIScrollView's state, so there is no necessity to use a delegate (and you can use this delegate for whatever else you need).
Please try this solution of mine and let me know why this ain't as good as the previous answers.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
if (fabs(velocity.y) > 1)
[self hideTopBar:(velocity.y > 0)];
}
- (void)hideTopBar:(BOOL)hide
{
[self.navigationController setNavigationBarHidden:hide animated:YES];
[[UIApplication sharedApplication] setStatusBarHidden:hide withAnimation:UIStatusBarAnimationSlide];
}
One way that I’ve accomplished this is the following.
Register your view controller to be the UIScrollViewDelegate of your UITableView for example.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
From within de UIScrollViewDelegate methods you can get the new contentOffset and translate your UINavigationBar up or down accordingly.
Setting the alpha of the subviews can also be done based on some threshold values and factors you can set and compute.
Hope it helps!
In addition to Iwburk's answer I added the following to fix the alpha issue on non custom navigation bars and to reset the navigation bar in the viewWillDisappear method:
- (void)updateBarButtonItems:(CGFloat)alpha
{
for (UIView *view in self.navigationController.navigationBar.subviews) {
NSString *className = NSStringFromClass([view class]);
if ( ![className isEqualToString:#"_UINavigationBarBackground"] ) {
view.alpha = alpha;
}
}
}
- (void)resetNavigationBar {
CGRect frame = self.navigationController.navigationBar.frame;
frame.origin.y = 20;
[self.navigationController.navigationBar setFrame:frame];
[self updateBarButtonItems:1.0f];
}
I was looking for a solution that allowed for any style and any behavior. You'll notice that bar condensing behavior is different in many different apps. And of course, the way the bar looks is totally different between apps.
I created a solution for this issue with https://github.com/bryankeller/BLKFlexibleHeightBar/
You can definine your own behavior rules to control how and when the bar shrinks and grows, and you can define exactly how you want the bar's subviews to react to the bar condensing or growing.
Have a look at my project if you want a lot of flexibility to make whatever kind of header bar you can think up.
I was trying to emulate this behavior in a situation where I needed a customized header sitting about a UITableView. I rolled my own "navigation" bar because this sits below a bunch of other stuff on the page and I wanted the section headers to follow the default "docking" behavior. I think I found a pretty clever and succinct way to adjust a UITableView/UIScrollView together with another object in a style similar to that seen in the Facebook/Instagram/Chrome/etc. apps.
In my .xib file, I have my components loaded into a freeform view: http://imgur.com/0z9yebJ (sorry, don't have the rep to inline images)
Notice that, in the left sidebar, the table is ordered behind the main header view. You can't tell from the screenshot, but it also has the same y position as the main header view. Since it extends out of sight, the contentInset property on the UITableView set to 76 (the height of the main header view).
To make the main header view slide up in unison with the UIScrollView, I use the UIScrollViewDelegate's scrollViewDidScroll methods to perform some calculations and change the UIScrollView's contentInset as well as the main header view's frame.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
UIEdgeInsets insets = scrollView.contentInset;
//tableViewInsetDelta and tableViewOriginalInsetValue are NSInteger variables that I set to 0 and 76, respectively, in viewDidLoad
tableViewInsetDelta = tableViewOriginalInsetValue + scrollView.contentOffset.y;
insets.top = tableViewOriginalInsetValue - tableViewInsetDelta;
if (scrollView.contentOffset.y > -76 && scrollView.contentOffset.y < 0) {
[scrollView setContentInset:insets];
self.pathTitleContainer.frame = CGRectMake(self.pathTitleContainer.frame.origin.x, 44 - tableViewInsetDelta, self.pathTitleContainer.frame.size.width, self.pathTitleContainer.frame.size.height);
} else if (scrollView.contentOffset.y > 0) {
insets.top = 0;
[scrollView setContentInset:insets];
self.pathTitleContainer.frame = CGRectMake(self.pathTitleContainer.frame.origin.x, -32, self.pathTitleContainer.frame.size.width, self.pathTitleContainer.frame.size.height);
} else if (scrollView.contentOffset.y < -76) {
insets.top = 76;
[scrollView setContentInset:insets];
self.pathTitleContainer.frame = CGRectMake(self.pathTitleContainer.frame.origin.x, 44, self.pathTitleContainer.frame.size.width, self.pathTitleContainer.frame.size.height);
}
}
The first if statement does most of the heavy lifting, but I had to include the other two to handle situations where the user is dragging forcefully and the initial contentOffset values sent to scrollViewDidScroll are outside of the range of the first if statement.
Ultimately, this is working really well for me. I hate loading up my projects with a bunch of bloated subclasses. I can't speak to whether this is the best solution performance-wise (I've always been hesitant to put any code in scrollViewDidScroll since it gets called all the time), but the code footprint is the smallest I've seen in any solution for this problem and it doesn't involve nesting a UITableView in a UIScrollView (Apple advises against this in the documentation and touch events end up a bit funky on the UITableView). Hope this helps someone!
HidingNavigationBar a great project that hides the Navigation Bar and the Tab Bar if you want.
HidingNavigationBar supports hiding/showing of the following view
elements:
UINavigationBar
UINavigationBar and an extension UIView
UINavigationBar and a UIToolbar
UINavigationBar and a UITabBar
https://github.com/tristanhimmelman/HidingNavigationBar
I tried implementing GTScrollNavigationBar but my app required me to modify auto layout constraints. I decided to put an example of my implementation up on GitHub in case anyone else has to do this with auto layout. The other issue I had with most of the other implementations is that people don't set the bounds of the scroll view to avoid the parallax scrolling effect that you create while you scroll and adjust the size of the scrollview simultaneously.
Check out JSCollapsingNavBarViewController if you need to do this with auto layout. I've included two versions, one with the nav bar only and another with a sub-bar below the nav bar which collapses before collapsing the nav bar.
for Swift 4,5 - iOS 11 and above
private var previousScrollViewYOffset: CGFloat = 0
private var firstLoad = true
// to avoid scrollViewDidScroll called when first time view controller load
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
firstLoad = false
}
// MARK: - UIScrollViewDelegate
extension ViewController: UIScrollViewDelegate {
func stoppedScrolling() {
let frame = self.navigationController?.navigationBar.frame ?? .zero
if frame.origin.y < UIView.statusBarFrame.size.height {
self.animateNavBar(to: -frame.size.height + UIView.statusBarFrame.size.height)
}
}
func updateBarButtonItems(alpha: CGFloat) {
self.navigationItem.leftBarButtonItems?.forEach{ item in
item.customView?.alpha = alpha
}
self.navigationItem.rightBarButtonItems?.forEach{ item in
item.customView?.alpha = alpha
}
self.navigationItem.titleView?.alpha = alpha
self.navigationController?.navigationBar.tintColor = self.navigationController?.navigationBar.tintColor.withAlphaComponent(alpha)
}
func animateNavBar(to y: CGFloat) {
UIView.animate(withDuration: 0.2) {[weak self] in
var frame: CGRect = self?.navigationController?.navigationBar.frame ?? .zero
let alpha: CGFloat = frame.origin.y >= y ? 0 : 1
frame.origin.y = y
self?.navigationController?.navigationBar.frame = frame
self?.updateBarButtonItems(alpha: alpha)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if firstLoad { return }
var frame = self.navigationController?.navigationBar.frame ?? .zero
let size = frame.size.height - UIView.statusBarFrame.size.height
let framePercentageHidden = (UIView.statusBarFrame.size.height - frame.origin.y) / (frame.size.height - 1)
let scrollOffset = scrollView.contentOffset.y
let scrollDiff = scrollOffset - previousScrollViewYOffset
let scrollHeight = scrollView.frame.size.height
let scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom
if scrollOffset <= -scrollView.contentInset.top {
frame.origin.y = UIView.statusBarFrame.size.height
} else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
frame.origin.y = -size
} else {
frame.origin.y = min(UIView.statusBarFrame.size.height, max(-size, frame.origin.y - scrollDiff))
}
self.navigationController?.navigationBar.frame = frame
self.updateBarButtonItems(alpha: 1 - framePercentageHidden)
self.previousScrollViewYOffset = scrollOffset
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.stoppedScrolling()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if(!decelerate) {
self.stoppedScrolling()
}
}
}
UIView extension
extension UIView {
public static var statusBarFrame: CGRect {
get {
return UIApplication.shared.statusBarFrame
}
}
}
You should custom navigationItem.titleView to apply set alpha
i tried it with this way, i hope it will help.
just implement the code in delegate method and set to to the desired view/subview
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
CGRect frame=self.view.frame;
CGRect resultFrame=CGRectZero;
if(scrollView.contentOffset.y==0 || scrollView.contentOffset.y<0){
self.lastContentOffset=0;
self.offset=0;
resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
// Pass the resultFrame
[self showHide:YES withFrame:resultFrame];
}else if (self.lastContentOffset > scrollView.contentOffset.y){
NSNumber *temp=[NSNumber numberWithDouble:self.lastContentOffset-scrollView.contentOffset.y];
if(temp.intValue>40 || self.offset.intValue<temp.intValue){
self.offset=[NSNumber numberWithInt:0];
resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
// Pass the resultFrame
[self showHide:YES withFrame:resultFrame];
}else{
if(temp.intValue>0){
self.offset=[NSNumber numberWithInt:self.offset.intValue-temp.intValue];
resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
// Pass the resultFrame
[self showHide:YES withFrame:resultFrame];
}
}
}else if (self.lastContentOffset < scrollView.contentOffset.y){
NSNumber *temp=[NSNumber numberWithDouble:scrollView.contentOffset.y-self.lastContentOffset];
if(self.offset.intValue>40 || (self.offset.intValue+temp.intValue)>40){
self.offset=[NSNumber numberWithInt:40];
// Pass the resultFrame
[self showHide:NO withFrame:resultFrame];
}else{
self.offset=[NSNumber numberWithInt:self.offset.intValue+temp.intValue];
resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
// Pass the resultFrame
[self showHide:YES withFrame:resultFrame];
}
}
self.lastContentOffset = scrollView.contentOffset.y;
}
-(void)showHide:(Boolean)boolView withFrame:(CGRect)frame{
if(showSRPFilter){
//Assign value of "frame"to any view on which you wan to to perform animation
}else{
//Assign value of "frame"to any view on which you wan to to perform animation
}
}
An extension of #Iwburk 's answer... Instead of changing the origin of the navigation bar, I needed to expand/shrink the size of the navigation bar.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect frame = self.previousRect; // a property set in the init method to hold the initial size of the uinavigationbar
CGFloat size = frame.size.height;
CGFloat framePercentageHidden = ((MINIMUMNAVBARHEIGHT - frame.origin.y) / (frame.size.height - 1));
CGFloat scrollOffset = scrollView.contentOffset.y;
CGFloat scrollDiff = scrollOffset - self.previousScrollViewYOffset;
CGFloat scrollHeight = scrollView.frame.size.height;
CGFloat scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom;
if (scrollOffset <= -scrollView.contentInset.top) {
frame.origin.y = -MINIMUMNAVBARHEIGHT;
} else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
frame.origin.y = -size;
} else {
frame.origin.y = MIN(-MINIMUMNAVBARHEIGHT, MAX(-size, frame.origin.y - scrollDiff));
}
self.previousRect = CGRectMake(0, frame.origin.y, self.jsExtendedBarView.frame.size.width, 155);
self.layoutConstraintExtendedViewHeight.constant = MAXIMUMNAVBARHEIGHT + frame.origin.y + MINIMUMNAVBARHEIGHT;
[self updateBarButtonItems:(1 - framePercentageHidden)];
self.previousScrollViewYOffset = scrollOffset;
}
It doesn't work with the stoppedScrolling method yet, ill post an update when I have it
All of these approaches seem overly complicated... So naturally, I built my own:
class ViewController: UIViewController, UIScrollViewDelegate {
var originalNavbarHeight:CGFloat = 0.0
var minimumNavbarHeight:CGFloat = 0
weak var scrollView:UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// setup delegates
scrollView.delegate = self
// save the original nav bar height
originalNavbarHeight = navigationController!.navigationBar.height
}
func scrollViewDidScroll(scrollView: UIScrollView) {
// will relayout subviews
view.setNeedsLayout() // calls viewDidLayoutSubviews
}
override func viewDidLayoutSubviews() {
var percentageScrolled = min(scrollView.contentOffset.y / originalNavbarHeight, 1)
navigationController?.navigationBar.height = min(max((1 - percentageScrolled) * originalNavbarHeight, minimumNavbarHeight), originalNavbarHeight)
// re-position and scale scrollview
scrollView.y = navigationController!.navigationBar.height + UIApplication.sharedApplication().statusBarFrame.height
scrollView.height = view.height - scrollView.y
}
override func viewWillDisappear(animated: Bool) {
navigationController?.navigationBar.height = originalNavbarHeight
}
}
I found all answers given in Objective-C. This is my answer in Swift 3. This is very generic code and can be used directly. It works with both UIScrollView and UITableView.
var lastContentOffset: CGPoint? = nil
var maxMinus: CGFloat = -24.0
var maxPlus: CGFloat = 20.0
var initial: CGFloat = 0.0
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Alarm Details"
self.lastContentOffset = self.alarmDetailsTableView.contentOffset
initial = maxPlus
}
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
var navigationBarFrame: CGRect = self.navigationController!.navigationBar.frame
let currentOffset = scrollView.contentOffset
if (currentOffset.y > (self.lastContentOffset?.y)!) {
if currentOffset.y > 0 {
initial = initial - fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
}
else if scrollView.contentSize.height < scrollView.frame.size.height {
initial = initial + fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
}
}
else {
if currentOffset.y < scrollView.contentSize.height - scrollView.frame.size.height {
initial = initial + fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
}
else if scrollView.contentSize.height < scrollView.frame.size.height && initial < maxPlus {
initial = initial - fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
}
}
initial = (initial <= maxMinus) ? maxMinus : initial
initial = (initial >= maxPlus) ? maxPlus : initial
navigationBarFrame.origin.y = initial
self.navigationController!.navigationBar.frame = navigationBarFrame
scrollView.frame = CGRect(x: 0.0, y: initial + navigationBarFrame.size.height , width: navigationBarFrame.size.width, height: self.view.frame.size.height - (initial + navigationBarFrame.size.height))
let framePercentageHidden: CGFloat = ((20 - navigationBarFrame.origin.y) / (navigationBarFrame.size.height));
self.lastContentOffset = currentOffset;
self.updateBarButtonItems(alpha: 1 - framePercentageHidden)
}
func updateBarButtonItems(alpha: CGFloat)
{
self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.darkGray.withAlphaComponent(alpha)]
self.navigationController?.navigationBar.isUserInteractionEnabled = (alpha < 1) ? false: true
guard (self.navigationItem.leftBarButtonItems?.count) != nil else { return }
for (_, value) in self.navigationItem.leftBarButtonItems!.enumerated() {
value.customView?.alpha = alpha
}
guard (self.navigationItem.rightBarButtonItems?.count) != nil else { return }
for (_, value) in (self.navigationItem.rightBarButtonItems?.enumerated())! {
value.customView?.alpha = alpha
}
}
The logic of setting alpha to navigation items is copied from #WayneBurkett answer and rewritten in Swift 3.
Related
I've implemented a custom header view that is not part of a UITableView. However, it responds to scrollView delegate methods to adjust the NSLayoutConstraint constant of it's height. The header view contains a web view that renders a static image. The header view moves on and off the screen as expected during tableView scrolling. When scrolling is fast (up/down) it works as expected. However, if you scroll down slowly the content of the UIWebview disappears.
I had initially thought it may have something to do with the webView's scrollView.contentSize but that seems to be adjusting correctly (based on NSLog). If you take a look at the above gif, the orange is the background of the UIWebview, so only the actual content of the html is vanishing; the view itself is intact. Anyone else run into this issue?
Relevant code:
#define kMaxHeaderHeight 160.0
#define kMinHeaderHeight 0.0
-(void)setupWebview
{
self.webView.scrollView.scrollEnabled = false;
self.webView.backgroundColor = [UIColor orangeColor];
NSString *html =
#"<head>"
#"<style>"
#"body {"
#"padding: 0px;"
#"font-family: Arial;"
#"width: 100%;"
#"margin: 0px;"
#"text-align: center;"
#"}"
#".image {"
#"position: absolute;"
#"margin: 0px;"
#"bottom: 0;"
#"padding: 0px;"
#"}"
#"</style>"
#"</head>"
#"<body>"
#"<a>"
#"<img class='image' src='http://mgk.com/wp-content/uploads/2013/05/bedbug_ICON.png' />"
#"</a>"
#"</body>";
[self.webView loadHTMLString:html baseURL:nil];
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView != self.tableView) return;
CGFloat scrollDiff = scrollView.contentOffset.y - previousScrollOffset;
CGFloat absoluteTop = 0.0;
CGFloat absoluteBottom = scrollView.contentSize.height - scrollView.frame.size.height;
BOOL isScrollingDown = scrollDiff > 0 && scrollView.contentOffset.y > absoluteTop;
BOOL isScrollingUp = scrollDiff < 0 && scrollView.contentOffset.y < absoluteBottom;
CGFloat newHeight = self.headerViewHeightConstraint.constant;
if (isScrollingDown) {
newHeight = MAX(kMinHeaderHeight, self.headerViewHeightConstraint.constant - fabs(scrollDiff));
} else if (isScrollingUp) {
newHeight = MIN(kMaxHeaderHeight, self.headerViewHeightConstraint.constant + fabs(scrollDiff));
}
if (newHeight != self.headerViewHeightConstraint.constant) {
self.headerViewHeightConstraint.constant = newHeight;
[self updateScrollPosition:previousScrollOffset];
}
previousScrollOffset = scrollView.contentOffset.y;
}
-(void)updateScrollPosition:(CGFloat)postition
{
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, postition);
}
What I've tried:
forcing the webview to layoutSubviews [self.webView layoutSubViews];
collecting all subviews of UIWebview into an array and adjust their height as the scrolling happens.
adjusting the contentView of the webView.scrollView as scrolling happens.
a bunch of other stuff i can't remember. suggest something and I'll tell you if i've tried it.
OP was able to resolve the issue by using WKWebView instead of UIWebView.
Can you force the webview to layout when changing the tableView contentOffset?
[self.webview setNeedsLayout]
I'm trying to limit the scroll speed of my UITableView, exactly like Instagram does it.
If you check out Instagram, you'll notice that they have a limit on how fast you can scroll through the feed.
It's not set using "decelerationRate" since the limit doesn't affect the deceleration. It simply affects the how fast you can scroll through the feed. So if you try to do a "flick" gesture, you will hit Instagrams max scrolling speed and won't go as fast as in a normal UITableView.
Any guesses on how Instagram accomplishes this?
TableView has a property scrollView, This property will return internal scrollView of TableView. Use following...
tableview.scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
ANOTHER WAY:
TableView will respond to scrollView delegate, so we need to implement scrollView's delegate like:
Take these global variables :
CGPoint lastOffset;
NSTimeInterval lastOffsetCapture;
BOOL isScrollingFast;
Implement scrollViewDidScroll like :
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint currentOffset = scrollView.contentOffset;
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval timeDiff = currentTime - lastOffsetCapture;
if(timeDiff > 0.1) {
CGFloat distance = currentOffset.y - lastOffset.y;
//The multiply by 10, / 1000 isn't really necessary.......
CGFloat scrollSpeedNotAbs = (distance * 10) / 1000; //in pixels per millisecond
CGFloat scrollSpeed = fabsf(scrollSpeedNotAbs);
if (scrollSpeed > 0.5) {
isScrollingFast = YES;
NSLog(#"Fast");
} else {
isScrollingFast = NO;
NSLog(#"Slow");
}
lastOffset = currentOffset;
lastOffsetCapture = currentTime;
}
}
Then implement scrollViewDidEndDragging like this :
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if( !decelerate )
{
NSUInteger currentIndex = (NSUInteger)(scrollView.contentOffset.x / scrollView.bounds.size.width);
[scrollView setContentOffset:CGPointMake(scrollView.bounds.size.width * currentIndex, 0) animated:YES];
}
}
Hope this may help you...
Use this:
self.tableview.scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
As tableView is a subclass of UIScrollView, ScrollView delegate will work here. Hope this helps.. :)
Edit:
if tableView doesn't show scrollView property use:
self.tableView.decelerationRate
In Swift set like this
tableView.decelerationRate = UIScrollView.DecelerationRate(rawValue: 0.5)
UIScrollView.DecelerationRate having two rate types normal and fast. By default its normal (approx value >= 0.9). didn't check about fast value.
To do like Instagram you need to check velocity using scrollViewWillEndDragging method.
check velocity, if its above some threshold then set decelerationRate as per your need.
I'm having a lot of trouble figuring out how best to reposition my UIScrollView's image view (I have a gallery kind of app going right now, similar to Photos.app, specifically when you're viewing a single image) when the orientation switches from portrait to landscape or vice-versa.
I know my best bet is to manipulate the contentOffset property, but I'm not sure what it should be changed to.
I've played around a lot, and it seems like for whatever reason 128 works really well. In my viewWillLayoutSubviews method for my view controller I have:
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.x >= 128) {
newContentOffset.x -= 128.0;
}
else {
newContentOffset.x = 0.0;
}
newContentOffset.y += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
else {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.y >= 128) {
newContentOffset.y -= 128.0;
}
else {
newContentOffset.y = 0.0;
}
newContentOffset.x += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
And it works pretty well. I hate how it's using a magic number though, and I have no idea where this would come from.
Also, whenever I zoom the image I have it set to stay centred (just like Photos.app does):
- (void)centerScrollViewContent {
// Keep image view centered as user zooms
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(self.scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(self.scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(self.scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(self.scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
}
So I need it to keep it positioned properly so it doesn't show black borders around the image when repositioned. (That's what the checks in the first block of code are for.)
Basically, I'm curious how to implement functionality like in Photos.app, where on rotate the scrollview intelligently repositions the content so that the middle of the visible content before the rotation is the same post-rotation, so it feels continuous.
You should change the UIScrollView's contentOffset property whenever the scrollView is layouting its subviews after its bounds value has been changed. Then when the interface orientation will be changed, UIScrollView's bounds will be changed accordingly updating the contentOffset.
To make things "right" you should subclass UIScrollView and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
The contentOffset calculation function should be placed inside UIScrollView's layoutSubviews method. The problem is that this method is called not only when the bounds value is changed but also when srollView is zoomed or scrolled. So the bounds value should be tracked to hint if the layoutSubviews method is called due to a change in bounds as a consequence of the orientation change, or due to a pan or pinch gesture.
So the first part of the UIScrollView subclass should look like this:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews method is called every time the UIScrollView is panned, zoomed or its bounds are changed. The _centerScrollViewContent method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView class and therefore instead of using self.imageView property to reference the zoomed view, which may not be available in the context of UIScrollView class, the viewForZoomingInScrollView: delegate method is used.
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset method. This method is responsible for adjusting the contentOffset. Such that when UIScrollView's bounds value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView's bounds is changed (e.g.: orientation change).
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView class (here is link to GitHub) implementing the above behavior and additional bonuses:
You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
In addition to adjusting contentOffset you may find that you also want to adjust the scrollView's zoomScale. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.
I've found a good solution for looping my scrollview, but I'm also using a UIPageControl for showing what the actual page's number is.
I change the pageControl.currentPage property when the scrollview scrolls, so if you scroll the scrollview more than a half the pageControl shows the new page.
I'd like to keep this feature, but with my actual code the pageNumber is not correct.
I think I should use this: https://github.com/gblancogarcia/GBInfiniteScrollView
But I can't make it work...
Here's my code:
-(void)rotateIfNecessary{
if(scrollView.contentOffset.x == 0) {
CGPoint newOffset = CGPointMake(scrollView.bounds.size.width+scrollView.contentOffset.x, scrollView.contentOffset.y);
[scrollView setContentOffset:newOffset];
[self rotateViewsRight];
}
else if(scrollView.contentOffset.x == (scrollView.contentSize.width - scrollView.frame.size.width) ) {
CGPoint newOffset = CGPointMake(scrollView.contentOffset.x-scrollView.bounds.size.width, scrollView.contentOffset.y);
[scrollView setContentOffset:newOffset];
[self rotateViewsLeft];
}
}
-(void)rotateViewsRight {
NSMutableArray* controllers = [DashboardFrameUtil getFrameViewControllers];
DashboardFrameViewController *endView = [controllers lastObject];
[controllers removeLastObject];
[controllers insertObject:endView atIndex:0];
[self resizePages];
}
-(void)rotateViewsLeft {
NSMutableArray* controllers = [DashboardFrameUtil getFrameViewControllers];
DashboardFrameViewController *endView = [controllers firstObject];
[controllers removeObjectAtIndex:0];
[controllers addObject:endView];
[self resizePages];
}
Then I call rotateIfNecessary in my scrollViewDidEndDecelerating, everything works fine, but the page calculating.. (when the scrollview resizes itself, it calls the scrollViewDidScroll where I calculate the pageNumber:
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
pageControl.currentPage = page;
The pageControl doesn't show the correct page number when the scrollView loops.
Any idea?
Thank you in advance!
Let's start with:
if(scrollView.contentOffset.x == 0) {
CGPoint newOffset = CGPointMake(scrollView.bounds.size.width+scrollView.contentOffset.x, scrollView.contentOffset.y);
...
}
condition is true when scrollView.contentOffset.x == 0, so what for do you add scrollView.contentOffset.x (it's surely = 0) to scrollView.bounds.size.width ?
UPDATED
I didn't run code - there can be some mistakes
If I understand correctly when you, for example, rotate right - last element became first and you want LAST page to be showen?
If yes -- the problem with pages is because you change contentOffset all the time and also change 'data source' of scroll view, so there's no correlation between contentOffset and individual DashboardFrameViewController - same DashboardFrameViewController can be at different offsets in different moments of time. So You have to calculate page N according to something else.
For example:
Somewhere in ivar store initial controllers (with initial order):
- (void)viewDidLoad {
[super viewDidLoad];
_initialControllers = [[DashboardFrameUtil getFrameViewControllers] copy];
}
then you need some method to discover what vc is currently vissible or nearest to center (and store in ivar _currentlyVissibleVC), for example:
- (DashboardFrameViewController *)vissibleVC {
for (DashboardFrameViewController *vc in _initialControllers){
if (CGRectContainsPoint(vc.frame, CGPointMake(scrollView.contentOffset.x + scrollView.bounds.size.width/2, scrollView.contentOffset)){
_currentlyVissibleVC = vc;
return;
}
}
}
then you can calculate page num:
int pageN = [_initialControllers indexOfObject:_currentlyVissibleVC] + 1;
Hope helps
I would like to know if it is possible for me to scroll a UIScrollView on the click of a UIButton and if so, would someone be able to tell me how to.
currently I am using the below code to see if in the scrollview more content is there to its left and if it is there, display an image which would tell the users that there is more content if they scroll to the left.
I would like to implement a functionality where I add a UIButton instead of the image and when more content is available on the left and when the user clicks the button, the scrollview would scroll to its left.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView1 {
if (scrollView1.contentOffset.x == scrollView1.contentSize.width - scrollView1.frame.size.width)
{
// reached the right
self.imageView.hidden = YES;
}
else
{
self.imageView.hidden = NO;
}
}
It would be great if someone could help me out on this.
That's pretty easy, I think. Use:
[scrollView1 setContentOffset:CGPointMake(x, 0) animated:YES];
Where X is the value of scrollView1.contentOffset.x + scrollView1.contentSize.width.
for Objective C use:
-(IBAction)adjustScroll:(UIButton *)sender{
CGFloat xOffset = scrollView1.contentOffset.x;
CGFloat yOffset = scrollView1.contentOffset.y;
[scrollView1 setContentOffset:CGPointMake(xOffset + scrollView1.contentSize.width, yOffset) animated:YES];
}
for swift 4.2 use:
#IBAction func adjustScroll(sender:UIButton){
let xOffset = scrollView1.contentOffset.x
let yOffset = scrollView1.contentOffset.y
let customOffset = CGPoint(x: xOffset + scrollView1.contentSize.width, y: yOffset)
scrollView1.setContentOffset(customOffset, animated: true)
}