/* This file is adapted from GTScrollNavigationBar - https://github.com/luugiathuy/GTScrollNavigationBar * 3-clause BSD License * * Copyright (c) 2013, Luu Gia Thuy * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, this * list of conditions and the following disclaimer in the documentation and/or * other materials provided with the distribution. * * * Neither the name of the {organization} nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "GTScrollNavigationBar.h" #define kNearZero 0.000001f @interface GTScrollNavigationBar () @property (strong, nonatomic) UIPanGestureRecognizer* panGesture; @property (assign, nonatomic) CGFloat lastContentOffsetY; @end @implementation GTScrollNavigationBar - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self setup]; } return self; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setup]; } return self; } - (void)setup { self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; self.panGesture.delegate = self; self.panGesture.cancelsTouchesInView = NO; NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; [defaultCenter addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; [defaultCenter addObserver:self selector:@selector(statusBarOrientationDidChange) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; } - (void)dealloc { if (!SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"9.0")) { [[NSNotificationCenter defaultCenter] removeObserver:self]; } } #pragma mark - Properties - (void)setScrollView:(UIScrollView*)scrollView { [self resetToDefaultPositionWithAnimation:NO]; _scrollView = scrollView; // remove gesture from current panGesture's view if (self.panGesture.view) { [self.panGesture.view removeGestureRecognizer:self.panGesture]; } if (scrollView) { [scrollView addGestureRecognizer:self.panGesture]; } } #pragma mark - Public methods - (void)resetToDefaultPositionWithAnimation:(BOOL)animated { self.scrollState = GTScrollNavigationBarStateNone; CGRect frame = self.frame; frame.origin.y = [self statusBarTopOffset]; [self setFrame:frame alpha:1.0f animated:animated]; } #pragma mark - Notifications - (void)statusBarOrientationDidChange { [self resetToDefaultPositionWithAnimation:NO]; } - (void)applicationDidBecomeActive { [self resetToDefaultPositionWithAnimation:NO]; } #pragma mark - UIGestureRecognizerDelegate - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; } #pragma mark - panGesture handler - (void)handlePan:(UIPanGestureRecognizer*)gesture { if (!self.scrollView || gesture.view != self.scrollView) { return; } // Don't try to scroll navigation bar if there's not enough room if (self.scrollView.frame.size.height + (self.bounds.size.height * 2) >= self.scrollView.contentSize.height) { return; } CGFloat contentOffsetY = self.scrollView.contentOffset.y; // Reset scrollState when the gesture began if (gesture.state == UIGestureRecognizerStateBegan) { self.scrollState = GTScrollNavigationBarStateNone; self.lastContentOffsetY = contentOffsetY; return; } CGFloat deltaY = contentOffsetY - self.lastContentOffsetY; if (deltaY < 0.0f) { self.scrollState = GTScrollNavigationBarStateScrollingDown; } else if (deltaY > 0.0f) { self.scrollState = GTScrollNavigationBarStateScrollingUp; } CGRect frame = self.frame; CGFloat alpha = 1.0f; CGFloat maxY = [self statusBarTopOffset]; CGFloat minY = maxY - CGRectGetHeight(frame) + 1.0f; // NOTE: plus 1px to prevent the navigation bar disappears in iOS < 7 CGFloat contentInsetTop = self.scrollView.contentInset.top; bool isBouncePastTopEdge = contentOffsetY < -contentInsetTop; if (isBouncePastTopEdge && CGRectGetMinY(frame) == maxY) { self.lastContentOffsetY = contentOffsetY; return; } bool isScrolling = (self.scrollState == GTScrollNavigationBarStateScrollingUp || self.scrollState == GTScrollNavigationBarStateScrollingDown); bool gestureIsActive = (gesture.state != UIGestureRecognizerStateEnded && gesture.state != UIGestureRecognizerStateCancelled); if (isScrolling && !gestureIsActive) { // Animate navigation bar to end position if (self.scrollState == GTScrollNavigationBarStateScrollingDown) { frame.origin.y = maxY; alpha = 1.0f; } else if (self.scrollState == GTScrollNavigationBarStateScrollingUp) { frame.origin.y = minY; alpha = kNearZero; } [self setFrame:frame alpha:alpha animated:YES]; } // When panning down at beginning of scrollView and the bar is expanding, do not update lastContentOffsetY if (!isBouncePastTopEdge && CGRectGetMinY(frame) == maxY) { self.lastContentOffsetY = contentOffsetY; } } #pragma mark - helper methods - (CGFloat)statusBarTopOffset { CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame; CGFloat topOffset = MIN(CGRectGetMaxX(statusBarFrame), CGRectGetMaxY(statusBarFrame)); bool isInCallStatusBar = topOffset == 40.0f; if (isInCallStatusBar) { topOffset -= 20.0f; } return topOffset; } - (void)setFrame:(CGRect)frame alpha:(CGFloat)alpha animated:(BOOL)animated { if (animated) { [UIView beginAnimations:@"GTScrollNavigationBarAnimation" context:nil]; } CGFloat offsetY = CGRectGetMinY(frame) - CGRectGetMinY(self.frame); UIView *firstView = [self.subviews firstObject]; for (UIView *view in self.subviews) { bool isBackgroundView = view == firstView; bool isViewHidden = view.hidden || view.alpha == 0.0f; if (isBackgroundView || isViewHidden) continue; view.alpha = alpha; } self.frame = frame; if (self.scrollView) { CGRect parentViewFrame = self.scrollView.superview.frame; parentViewFrame.origin.y += offsetY; parentViewFrame.size.height -= offsetY; self.scrollView.superview.frame = parentViewFrame; } if (animated) { [UIView commitAnimations]; } } @end @implementation UINavigationController (GTScrollNavigationBarAdditions) @dynamic scrollNavigationBar; - (GTScrollNavigationBar *)scrollNavigationBar { return (GTScrollNavigationBar *)self.navigationBar; } @end