diff --git a/AMScrollingNavbar.podspec b/AMScrollingNavbar.podspec index 79f38cb8..1010dedd 100644 --- a/AMScrollingNavbar.podspec +++ b/AMScrollingNavbar.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AMScrollingNavbar" - s.version = "4.3.0" + s.version = "4.3.1" s.summary = "A custom UINavigationController that enables the scrolling of the navigation bar alongside the scrolling of an observed content view" s.description = <<-DESC A custom UINavigationController that enables the scrolling of the diff --git a/CHANGELOG.md b/CHANGELOG.md index d7aa7ee0..ceff46db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ All notable changes to this project will be documented in this file. `AMScrollingNavbar` adheres to [Semantic Versioning](http://semver.org/). -- `4.2.x` Releases - [4.2.0](#420) | [4.2.1](#421) | [4.2.2](#422) | [4.2.3](#423) | [4.2.4](#424) | [4.3.0](#430) +- `4.3.x` Releases - [4.3.0](#430) | [4.3.1](#431) +- `4.2.x` Releases - [4.2.0](#420) | [4.2.1](#421) | [4.2.2](#422) | [4.2.3](#423) | [4.2.4](#424) - `4.1.x` Releases - [4.1.0](#410) - `4.0.x` Releases - [4.0.0](#400) | [4.0.1](#401) | [4.0.2](#402) | [4.0.3](#403) | [4.0.4](#404) | [4.0.5](#405) - `3.4.x` Releases - [3.4.0](#340) | [3.4.1](#341) @@ -17,6 +18,10 @@ All notable changes to this project will be documented in this file. --- +## [4.3.1](https://github.com/andreamazz/AMScrollingNavbar/releases/tag/4.3.1) + +- Merge #310 + ## [4.3.0](https://github.com/andreamazz/AMScrollingNavbar/releases/tag/4.3.0) - Fix orientation change issues diff --git a/Demo/Pods/AMScrollingNavbar/LICENSE b/Demo/Pods/AMScrollingNavbar/LICENSE new file mode 100644 index 00000000..6b6e3efa --- /dev/null +++ b/Demo/Pods/AMScrollingNavbar/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Andrea Mazzini + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Demo/Pods/AMScrollingNavbar/README.md b/Demo/Pods/AMScrollingNavbar/README.md new file mode 100644 index 00000000..f8bef933 --- /dev/null +++ b/Demo/Pods/AMScrollingNavbar/README.md @@ -0,0 +1,199 @@ +
+ +
+ +[![CocoaPods](https://cocoapod-badges.herokuapp.com/v/AMScrollingNavbar/badge.svg)](http://www.cocoapods.org/?q=amscrollingnavbar) +[![Build Status](https://travis-ci.org/andreamazz/AMScrollingNavbar.svg)](https://travis-ci.org/andreamazz/AMScrollingNavbar) +[![codecov.io](https://codecov.io/github/andreamazz/AMScrollingNavbar/coverage.svg?branch=master)](https://codecov.io/github/andreamazz/AMScrollingNavbar?branch=master) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +![Swift 4](https://img.shields.io/badge/swift-4-orange.svg) +[![Join the chat at https://gitter.im/andreamazz/AMScrollingNavbar](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/andreamazz/AMScrollingNavbar?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=46FNZD4PDVNRU) + +A custom UINavigationController that enables the scrolling of the navigation bar alongside the +scrolling of an observed content view + ++ + + +
+ +### Versioning notes + +- Version `2.x` is written as a subclass of `UINavigationController`, in Swift. +- Version `2.0.0` introduce Swift 2.0 syntax. +- Version `3.0.0` introduce Swift 3.0 syntax. +- Version `4.0.0` introduce Swift 4.0 syntax. + +If you are looking for the category implementation in Objective-C, make sure to checkout version `1.x` and prior, although the `2.x` is recomended. + +# Screenshot + ++ +
+ +# Setup with CocoaPods + +``` +pod 'AMScrollingNavbar' + +use_frameworks! +``` + +# Setup with Carthage + +``` +github "andreamazz/AMScrollingNavbar" +``` + +## Usage + +Make sure to use a subclass of `ScrollingNavigationController` for your `UINavigationController`. Either set the class of your `UINavigationController` in your storyboard, or create programmatically a `ScrollingNavigationController` instance in your code. + +Use `followScrollView(_: delay:)` to start following the scrolling of a scrollable view (e.g.: a `UIScrollView` or `UITableView`). +#### Swift +```swift +override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.followScrollView(tableView, delay: 50.0) + } +} +``` + +#### Objective-C +```objc +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [(ScrollingNavigationController *)self.navigationController followScrollView:self.tableView delay:50.0f]; +} +``` + +Use `stopFollowingScrollview()` to stop the behaviour. Remember to call this function on disappear: +```swift +override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.stopFollowingScrollView() + } +} +``` + +## ScrollingNavigationViewController +To DRY things up you can let your view controller subclass `ScrollingNavigationViewController`, which provides the base setup implementation. You will just need to call `followScrollView(_: delay:)`: +```swift +override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.followScrollView(tableView, delay: 50.0) + } +} +``` + +## Followers +To move another view, like a toolbar, alongside the navigation bar you can provide the view or multiple views as the `followers` parameter: +```swift +if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.followScrollView(tableView, delay: 50.0, followers: [toolbar]) +} +``` +Note that when navigating away from the controller the followers might keep the scroll offset. Refer to [Handling navigation](https://github.com/andreamazz/AMScrollingNavbar#handling-navigation) for proper setup. + +## Scrolling the TabBar +You can also pass a `UITabBar` in the `followers` array: +```swift +if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.followScrollView(tableView, delay: 50.0, followers: [tabBarController.tabBar]) +} +``` + + +## ScrollingNavigationControllerDelegate +You can set a delegate to receive a call when the state of the navigation bar changes: +```swift +if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.scrollingNavbarDelegate = self +} +``` + +Delegate function: +```swift +func scrollingNavigationController(_ controller: ScrollingNavigationController, didChangeState state: NavigationBarState) { + switch state { + case .collapsed: + print("navbar collapsed") + case .expanded: + print("navbar expanded") + case .scrolling: + print("navbar is moving") + } +} +``` + +## Handling navigation +If the view controller with the scroll view pushes new controllers, you should call `showNavbar(animated:)` in your `viewWillDisappear(animated:)`: +```swift +override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.showNavbar(animated: true) + } +} +``` + +## Scrolling to top +When the user taps the status bar, by default a scrollable view scrolls to the top of its content. If you want to also show the navigation bar, make sure to include this in your controller: + +```swift +func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + if let navigationController = navigationController as? ScrollingNavigationController { + navigationController.showNavbar(animated: true) + } + return true +} +``` + +## Scroll speed +You can control the speed of the scrolling using the `scrollSpeedFactor` optional parameter: + +```swift +controller.followScrollView(view, delay: 0, scrollSpeedFactor: 2) +``` + +Check out the sample project for more details. + +# Author +[Andrea Mazzini](https://twitter.com/theandreamazz). I'm available for freelance work, feel free to contact me. + +Want to support the development of [these free libraries](https://cocoapods.org/owners/734)? Buy me a coffee ☕️ via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=46FNZD4PDVNRU). + +# Contributors +[Syo Ikeda](https://github.com/ikesyo) and [everyone](https://github.com/andreamazz/AMScrollingNavbar/graphs/contributors) kind enough to submit a pull request. + +# MIT License + The MIT License (MIT) + + Copyright (c) 2017 Andrea Mazzini + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavbar+Sizes.swift b/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavbar+Sizes.swift new file mode 100644 index 00000000..4649a2b6 --- /dev/null +++ b/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavbar+Sizes.swift @@ -0,0 +1,67 @@ +import UIKit +import WebKit + +/** + Implements the main functions providing constants values and computed ones + */ +extension ScrollingNavigationController { + + // MARK: - View sizing + + var fullNavbarHeight: CGFloat { + return navbarHeight + statusBarHeight + } + + var navbarHeight: CGFloat { + return navigationBar.frame.size.height + } + + var statusBarHeight: CGFloat { + var statusBarHeight = UIApplication.shared.statusBarFrame.size.height + if #available(iOS 11.0, *) { + // Account for the notch when the status bar is hidden + statusBarHeight = max(UIApplication.shared.statusBarFrame.size.height, UIApplication.shared.delegate?.window??.safeAreaInsets.top ?? 0) + } + return statusBarHeight - extendedStatusBarDifference + } + + // Extended status call changes the bounds of the presented view + var extendedStatusBarDifference: CGFloat { + return abs(view.bounds.height - (UIApplication.shared.delegate?.window??.frame.size.height ?? UIScreen.main.bounds.height)) + } + + var tabBarOffset: CGFloat { + // Only account for the tab bar if a tab bar controller is present and the bar is not translucent + if let tabBarController = tabBarController { + return tabBarController.tabBar.isTranslucent ? 0 : tabBarController.tabBar.frame.height + } + return 0 + } + + func scrollView() -> UIScrollView? { + if let webView = self.scrollableView as? UIWebView { + return webView.scrollView + } else if let wkWebView = self.scrollableView as? WKWebView { + return wkWebView.scrollView + } else { + return scrollableView as? UIScrollView + } + } + + var contentOffset: CGPoint { + return scrollView()?.contentOffset ?? CGPoint.zero + } + + var contentSize: CGSize { + guard let scrollView = scrollView() else { + return CGSize.zero + } + + let verticalInset = scrollView.contentInset.top + scrollView.contentInset.bottom + return CGSize(width: scrollView.contentSize.width, height: scrollView.contentSize.height + verticalInset) + } + + var deltaLimit: CGFloat { + return navbarHeight - statusBarHeight + } +} diff --git a/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavigationController.swift b/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavigationController.swift new file mode 100644 index 00000000..cd7d0813 --- /dev/null +++ b/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavigationController.swift @@ -0,0 +1,527 @@ +import UIKit + +/** + Scrolling Navigation Bar delegate protocol + */ +@objc public protocol ScrollingNavigationControllerDelegate: NSObjectProtocol { + /// Called when the state of the navigation bar changes + /// + /// - Parameters: + /// - controller: the ScrollingNavigationController + /// - state: the new state + @objc optional func scrollingNavigationController(_ controller: ScrollingNavigationController, didChangeState state: NavigationBarState) + + /// Called when the state of the navigation bar is about to change + /// + /// - Parameters: + /// - controller: the ScrollingNavigationController + /// - state: the new state + @objc optional func scrollingNavigationController(_ controller: ScrollingNavigationController, willChangeState state: NavigationBarState) +} + +/** + The state of the navigation bar + + - collapsed: the navigation bar is fully collapsed + - expanded: the navigation bar is fully visible + - scrolling: the navigation bar is transitioning to either `Collapsed` or `Scrolling` + */ +@objc public enum NavigationBarState: Int { + case collapsed, expanded, scrolling +} + +/** + The direction of scrolling that the navigation bar should be collapsed. + The raw value determines the sign of content offset depending of collapse direction. + + - scrollUp: scrolling up direction + - scrollDown: scrolling down direction + */ +@objc public enum NavigationBarCollapseDirection: Int { + case scrollUp = -1 + case scrollDown = 1 +} + +/** + A custom `UINavigationController` that enables the scrolling of the navigation bar alongside the + scrolling of an observed content view + */ +@objcMembers +open class ScrollingNavigationController: UINavigationController, UIGestureRecognizerDelegate { + + /** + Returns the `NavigationBarState` of the navigation bar + */ + open fileprivate(set) var state: NavigationBarState = .expanded { + willSet { + if state != newValue { + scrollingNavbarDelegate?.scrollingNavigationController?(self, willChangeState: newValue) + } + } + didSet { + navigationBar.isUserInteractionEnabled = (state == .expanded) + if state != oldValue { + scrollingNavbarDelegate?.scrollingNavigationController?(self, didChangeState: state) + } + } + } + + /** + Determines whether the navbar should scroll when the content inside the scrollview fits + the view's size. Defaults to `false` + */ + open var shouldScrollWhenContentFits = false + + /** + Determines if the navbar should expand once the application becomes active after entering background + Defaults to `true` + */ + open var expandOnActive = true + + /** + Determines if the navbar scrolling is enabled. + Defaults to `true` + */ + open var scrollingEnabled = true + + /** + The delegate for the scrolling navbar controller + */ + open weak var scrollingNavbarDelegate: ScrollingNavigationControllerDelegate? + + /** + An array of `UIView`s that will follow the navbar + */ + open var followers: [UIView] = [] + + open fileprivate(set) var gestureRecognizer: UIPanGestureRecognizer? + fileprivate var sourceTabBar: UITabBar? + var delayDistance: CGFloat = 0 + var maxDelay: CGFloat = 0 + var scrollableView: UIView? + var lastContentOffset = CGFloat(0.0) + var scrollSpeedFactor: CGFloat = 1 + var collapseDirectionFactor: CGFloat = 1 // Used to determine the sign of content offset depending of collapse direction + var previousState: NavigationBarState = .expanded // Used to mark the state before the app goes in background + + /** + Start scrolling + + Enables the scrolling by observing a view + + - parameter scrollableView: The view with the scrolling content that will be observed + - parameter delay: The delay expressed in points that determines the scrolling resistance. Defaults to `0` + - parameter scrollSpeedFactor : This factor determines the speed of the scrolling content toward the navigation bar animation + - parameter collapseDirection : The direction of scrolling that the navigation bar should be collapsed + - parameter followers: An array of `UIView`s that will follow the navbar + */ + open func followScrollView(_ scrollableView: UIView, delay: Double = 0, scrollSpeedFactor: Double = 1, collapseDirection: NavigationBarCollapseDirection = .scrollDown, followers: [UIView] = []) { + self.scrollableView = scrollableView + + gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ScrollingNavigationController.handlePan(_:))) + gestureRecognizer?.maximumNumberOfTouches = 1 + gestureRecognizer?.delegate = self + scrollableView.addGestureRecognizer(gestureRecognizer!) + + NotificationCenter.default.addObserver(self, selector: #selector(ScrollingNavigationController.willResignActive(_:)), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ScrollingNavigationController.didBecomeActive(_:)), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ScrollingNavigationController.didRotate(_:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil) + + maxDelay = CGFloat(delay) + delayDistance = CGFloat(delay) + scrollingEnabled = true + + // Save TabBar state (the state is changed during the transition and restored on compeltion) + if let tab = followers.first(where: { $0 is UITabBar }) as? UITabBar { + self.sourceTabBar = UITabBar(frame: tab.frame) + self.sourceTabBar?.isTranslucent = tab.isTranslucent + } + self.followers = followers + self.scrollSpeedFactor = CGFloat(scrollSpeedFactor) + self.collapseDirectionFactor = CGFloat(collapseDirection.rawValue) + } + + /** + Hide the navigation bar + + - parameter animated: If true the scrolling is animated. Defaults to `true` + - parameter duration: Optional animation duration. Defaults to 0.1 + */ + open func hideNavbar(animated: Bool = true, duration: TimeInterval = 0.1) { + guard let _ = self.scrollableView, let visibleViewController = self.visibleViewController else { return } + + if state == .expanded { + self.state = .scrolling + UIView.animate(withDuration: animated ? duration : 0, animations: { () -> Void in + self.scrollWithDelta(self.fullNavbarHeight) + visibleViewController.view.setNeedsLayout() + if self.navigationBar.isTranslucent { + let currentOffset = self.contentOffset + self.scrollView()?.contentOffset = CGPoint(x: currentOffset.x, y: currentOffset.y + self.navbarHeight) + } + }) { _ in + self.state = .collapsed + } + } else { + updateNavbarAlpha() + } + } + + /** + Show the navigation bar + + - parameter animated: If true the scrolling is animated. Defaults to `true` + - parameter duration: Optional animation duration. Defaults to 0.1 + */ + open func showNavbar(animated: Bool = true, duration: TimeInterval = 0.1) { + guard let _ = self.scrollableView, let visibleViewController = self.visibleViewController else { return } + + if state == .collapsed { + gestureRecognizer?.isEnabled = false + let animations = { + self.lastContentOffset = 0; + self.scrollWithDelta(-self.fullNavbarHeight, ignoreDelay: true) + visibleViewController.view.setNeedsLayout() + if self.navigationBar.isTranslucent { + let currentOffset = self.contentOffset + self.scrollView()?.contentOffset = CGPoint(x: currentOffset.x, y: currentOffset.y - self.navbarHeight) + } + } + if animated { + self.state = .scrolling + UIView.animate(withDuration: duration, animations: animations) { _ in + self.state = .expanded + self.gestureRecognizer?.isEnabled = true + } + } else { + animations() + self.state = .expanded + self.gestureRecognizer?.isEnabled = true + } + } else { + updateNavbarAlpha() + } + } + + /** + Stop observing the view and reset the navigation bar + + - parameter showingNavbar: If true the navbar is show, otherwise it remains in its current state. Defaults to `true` + */ + open func stopFollowingScrollView(showingNavbar: Bool = true) { + if showingNavbar { + showNavbar(animated: true) + } + if let gesture = gestureRecognizer { + scrollableView?.removeGestureRecognizer(gesture) + } + scrollableView = .none + gestureRecognizer = .none + scrollingNavbarDelegate = .none + scrollingEnabled = false + + let center = NotificationCenter.default + center.removeObserver(self, name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) + center.removeObserver(self, name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil) + } + + // MARK: - Gesture recognizer + + func handlePan(_ gesture: UIPanGestureRecognizer) { + if gesture.state != .failed { + if let superview = scrollableView?.superview { + let translation = gesture.translation(in: superview) + let delta = collapseDirectionFactor * (lastContentOffset - translation.y) / scrollSpeedFactor + lastContentOffset = translation.y + + if shouldScrollWithDelta(delta) { + scrollWithDelta(delta) + } + } + } + + if gesture.state == .ended || gesture.state == .cancelled || gesture.state == .failed { + checkForPartialScroll() + lastContentOffset = 0 + } + } + + // MARK: - Rotation handler + + func didRotate(_ notification: Notification) { + showNavbar() + } + + /** + UIContentContainer protocol method. + Will show the navigation bar upon rotation or changes in the trait sizes. + */ + open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + showNavbar() + } + + // MARK: - Notification handler + + func didBecomeActive(_ notification: Notification) { + if expandOnActive { + showNavbar(animated: false) + } else { + if previousState == .collapsed { + hideNavbar(animated: false) + } + } + } + + func willResignActive(_ notification: Notification) { + previousState = state + } + + /// Handles when the status bar changes + func willChangeStatusBar() { + showNavbar(animated: true) + } + + // MARK: - Scrolling functions + + private func shouldScrollWithDelta(_ delta: CGFloat) -> Bool { + let scrollDelta = delta + // Check for rubberbanding + if scrollDelta < 0 { + if let scrollableView = scrollableView , contentOffset.y + scrollableView.frame.size.height > contentSize.height && scrollableView.frame.size.height < contentSize.height { + // Only if the content is big enough + return false + } + } + return true + } + + private func scrollWithDelta(_ delta: CGFloat, ignoreDelay: Bool = false) { + var scrollDelta = delta + let frame = navigationBar.frame + + // View scrolling up, hide the navbar + if scrollDelta > 0 { + // Update the delay + if !ignoreDelay { + delayDistance -= scrollDelta + + // Skip if the delay is not over yet + if delayDistance > 0 { + return + } + } + + // No need to scroll if the content fits + if !shouldScrollWhenContentFits && state != .collapsed && + (scrollableView?.frame.size.height)! >= contentSize.height { + return + } + + // Compute the bar position + if frame.origin.y - scrollDelta < -deltaLimit { + scrollDelta = frame.origin.y + deltaLimit + } + + // Detect when the bar is completely collapsed + if frame.origin.y <= -deltaLimit { + state = .collapsed + delayDistance = maxDelay + } else { + state = .scrolling + } + } + + if scrollDelta < 0 { + // Update the delay + if !ignoreDelay { + delayDistance += scrollDelta + + // Skip if the delay is not over yet + if delayDistance > 0 && maxDelay < contentOffset.y { + return + } + } + + // Compute the bar position + if frame.origin.y - scrollDelta > statusBarHeight { + scrollDelta = frame.origin.y - statusBarHeight + } + + // Detect when the bar is completely expanded + if frame.origin.y >= statusBarHeight { + state = .expanded + delayDistance = maxDelay + } else { + state = .scrolling + } + } + + updateSizing(scrollDelta) + updateNavbarAlpha() + restoreContentOffset(scrollDelta) + updateFollowers(scrollDelta) + updateContentInset(scrollDelta) + } + + /// Adjust the top inset (useful when a table view has floating headers, see issue #219 + private func updateContentInset(_ delta: CGFloat) { + if let contentInset = scrollView()?.contentInset, let scrollInset = scrollView()?.scrollIndicatorInsets { + scrollView()?.contentInset = UIEdgeInsets(top: contentInset.top - delta, left: contentInset.left, bottom: contentInset.bottom, right: contentInset.right) + scrollView()?.scrollIndicatorInsets = UIEdgeInsets(top: scrollInset.top - delta, left: scrollInset.left, bottom: scrollInset.bottom, right: scrollInset.right) + } + } + + private func updateFollowers(_ delta: CGFloat) { + followers.forEach { + guard let tabBar = $0 as? UITabBar else { + $0.transform = $0.transform.translatedBy(x: 0, y: -delta) + return + } + tabBar.isTranslucent = true + tabBar.frame.origin.y += delta * 1.5 + + // Set the bar to its original state if it's in its original position + if let originalTabBar = sourceTabBar, originalTabBar.frame.origin.y == tabBar.frame.origin.y { + tabBar.isTranslucent = originalTabBar.isTranslucent + } + } + } + + private func updateSizing(_ delta: CGFloat) { + guard let topViewController = self.topViewController else { return } + + var frame = navigationBar.frame + + // Move the navigation bar + frame.origin = CGPoint(x: frame.origin.x, y: frame.origin.y - delta) + navigationBar.frame = frame + + // Resize the view if the navigation bar is not translucent + if !navigationBar.isTranslucent { + let navBarY = navigationBar.frame.origin.y + navigationBar.frame.size.height + frame = topViewController.view.frame + frame.origin = CGPoint(x: frame.origin.x, y: navBarY) + frame.size = CGSize(width: frame.size.width, height: view.frame.size.height - (navBarY) - tabBarOffset) + topViewController.view.frame = frame + } + } + + private func restoreContentOffset(_ delta: CGFloat) { + if navigationBar.isTranslucent || delta == 0 { + return + } + + // Hold the scroll steady until the navbar appears/disappears + if let scrollView = scrollView() { + scrollView.setContentOffset(CGPoint(x: contentOffset.x, y: contentOffset.y - delta), animated: false) + } + } + + private func checkForPartialScroll() { + let frame = navigationBar.frame + var duration = TimeInterval(0) + var delta = CGFloat(0.0) + + // Scroll back down + let threshold = statusBarHeight - (frame.size.height / 2) + if navigationBar.frame.origin.y >= threshold { + delta = frame.origin.y - statusBarHeight + let distance = delta / (frame.size.height / 2) + duration = TimeInterval(abs(distance * 0.2)) + state = .expanded + } else { + // Scroll up + delta = frame.origin.y + deltaLimit + let distance = delta / (frame.size.height / 2) + duration = TimeInterval(abs(distance * 0.2)) + state = .collapsed + } + + delayDistance = maxDelay + + UIView.animate(withDuration: duration, delay: 0, options: UIViewAnimationOptions.beginFromCurrentState, animations: { + self.updateSizing(delta) + self.updateFollowers(delta) + self.updateNavbarAlpha() + self.updateContentInset(delta) + }, completion: nil) + } + + private func updateNavbarAlpha() { + guard let navigationItem = topViewController?.navigationItem else { return } + + let frame = navigationBar.frame + + // Change the alpha channel of every item on the navbr + let alpha = (frame.origin.y + deltaLimit) / frame.size.height + + // Hide all the possible titles + navigationItem.titleView?.alpha = alpha + navigationBar.tintColor = navigationBar.tintColor.withAlphaComponent(alpha) + if let titleColor = navigationBar.titleTextAttributes?[NSAttributedStringKey.foregroundColor] as? UIColor { + navigationBar.titleTextAttributes?[NSAttributedStringKey.foregroundColor] = titleColor.withAlphaComponent(alpha) + } else { + navigationBar.titleTextAttributes?[NSAttributedStringKey.foregroundColor] = UIColor.black.withAlphaComponent(alpha) + } + + // Hide all possible button items and navigation items + func shouldHideView(_ view: UIView) -> Bool { + let className = view.classForCoder.description().replacingOccurrences(of: "_", with: "") + var viewNames = ["UINavigationButton", "UINavigationItemView", "UIImageView", "UISegmentedControl"] + if #available(iOS 11.0, *) { + viewNames.append(navigationBar.prefersLargeTitles ? "UINavigationBarLargeTitleView" : "UINavigationBarContentView") + } else { + viewNames.append("UINavigationBarContentView") + } + return viewNames.contains(className) + } + + func setAlphaOfSubviews(view: UIView, alpha: CGFloat) { + view.alpha = alpha + view.subviews.forEach { setAlphaOfSubviews(view: $0, alpha: alpha) } + } + + navigationBar.subviews + .filter(shouldHideView) + .forEach { setAlphaOfSubviews(view: $0, alpha: alpha) } + + // Hide the left items + navigationItem.leftBarButtonItem?.customView?.alpha = alpha + navigationItem.leftBarButtonItems?.forEach { $0.customView?.alpha = alpha } + + // Hide the right items + navigationItem.rightBarButtonItem?.customView?.alpha = alpha + navigationItem.rightBarButtonItems?.forEach { $0.customView?.alpha = alpha } + } + + // MARK: - UIGestureRecognizerDelegate + + /** + UIGestureRecognizerDelegate function. Begin scrolling only if the direction is vertical (prevents conflicts with horizontal scroll views) + */ + open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return true } + let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view) + return fabs(velocity.y) > fabs(velocity.x) + } + + /** + UIGestureRecognizerDelegate function. Enables the scrolling of both the content and the navigation bar + */ + open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + /** + UIGestureRecognizerDelegate function. Only scrolls the navigation bar with the content when `scrollingEnabled` is true + */ + open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + return scrollingEnabled + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + +} diff --git a/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavigationViewController.swift b/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavigationViewController.swift new file mode 100644 index 00000000..b8aa9aa4 --- /dev/null +++ b/Demo/Pods/AMScrollingNavbar/Source/ScrollingNavigationViewController.swift @@ -0,0 +1,42 @@ +import UIKit + +/** + A custom `UIViewController` that implements the base configuration. + */ +open class ScrollingNavigationViewController: UIViewController, UIScrollViewDelegate, UINavigationControllerDelegate { + + // MARK: - ScrollView config + + /** + On appear calls `showNavbar()` by default + */ + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let navigationController = self.navigationController as? ScrollingNavigationController { + navigationController.showNavbar(animated: true) + } + } + + /** + On disappear calls `stopFollowingScrollView()` to stop observing the current scroll view, and perform the tear down + */ + override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if let navigationController = self.navigationController as? ScrollingNavigationController { + navigationController.stopFollowingScrollView() + } + } + + /** + Calls `showNavbar()` when a `scrollToTop` is requested + */ + open func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + if let navigationController = self.navigationController as? ScrollingNavigationController { + navigationController.showNavbar(animated: true) + } + return true + } + +} diff --git a/Demo/ScrollingNavbarDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/ScrollingNavbarDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Demo/ScrollingNavbarDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + +