Tap tab bar to scroll to top of UITableViewController - ios

Tapping the tab bar icon for the current navigation controller already returns the user to the root view, but if they are scrolled way down, if they tap it again I want it to scroll to the top (same effect as tapping the status bar). How would I do this?
A good example is Instagram's feed, scroll down then tap the home icon in the tab bar to scroll back to top.
The scrolling back to the top is easy, but connecting it to the tab bar controller is what I'm stuck on.

Implement the UITabBarControllerDelegate method tabBarController:didSelectViewController: to be notified when the user selects a tab. This method is also called when the same tab button is tapped again, even if that tab is already selected.
A good place to implement this delegate would probably be your AppDelegate. Or the object that logically "owns" the tab bar controller.
I would declare and implement a method that can be called on your view controllers to scroll the UICollectionView.
- (void)tabBarController:(UITabBarController *)tabBarController
didSelectViewController:(UIViewController *)viewController
{
static UIViewController *previousController = nil;
if (previousController == viewController) {
// the same tab was tapped a second time
if ([viewController respondsToSelector:#selector(scrollToTop)]) {
[viewController scrollToTop];
}
}
previousController = viewController;
}

SWIFT 3
Here goes..
First implement the UITabBarControllerDelegate in the class and make sure the delegate is set in viewDidLoad
class DesignStoryStreamVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UITabBarControllerDelegate {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
collectionView.delegate = self
collectionView.dataSource = self
}
}
Next, put this delegate function somewhere in your class.
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
print(tabBarIndex)
if tabBarIndex == 0 {
self.collectionView.setContentOffset(CGPoint.zero, animated: true)
}
}
Make sure to select the correct index in the "if" statement. I included the print function so you can double check.

Swift 5: no need for stored properties in the UITabBarController.
In MyTabBarController.swift, implement tabBarController(_:shouldSelect) to detect when the user re-selects the tab bar item:
protocol TabBarReselectHandling {
func handleReselect()
}
class MyTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
func tabBarController(
_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController
) -> Bool {
if tabBarController.selectedViewController === viewController,
let handler = viewController as? TabBarReselectHandling {
// NOTE: viewController in line above might be a UINavigationController,
// in which case you need to access its contents
handler.handleReselect()
}
return true
}
}
In MyTableViewController.swift, handle the re-selection by scrolling the table view to the top:
class MyTableViewController: UITableViewController, TabBarReselectHandling {
func handleReselect() {
tableView?.setContentOffset(.zero, animated: true)
}
}
Now you can easily extend this to other tabs by just implementing TabBarReselectHandling.

You can use shouldSelect rather than didSelect, which would omit the need for an external variable to keep track of the previous view controller.
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
if ([viewController isEqual:self] && [tabBarController.selectedViewController isEqual:viewController]) {
// Do custom stuff here
}
return YES;
}

extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: self.view)
}
}
This is my answer in Swift 3. It uses a helper function for recursive calls and it automatically scrolls to top on call. Tested on a UICollectionViewController embedded into a UINavigationController embedded in a UITabBarController

I was using this View hierarchy.
UITabBarController > UINavigationController > UIViewController
I got a reference to the UITabBarController in the UIViewController
tabBarControllerRef = self.tabBarController as! CustomTabBarClass
tabBarControllerRef!.navigationControllerRef = self.navigationController as! CustomNavigationBarClass
tabBarControllerRef!.viewControllerRef = self
Then I created a Bool that was called at the correct times, and a method that allows scrolling to top smoothly
var canScrollToTop:Bool = true
// Called when the view becomes available
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
canScrollToTop = true
}
// Called when the view becomes unavailable
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
canScrollToTop = false
}
// Scrolls to top nicely
func scrollToTop() {
self.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
Then in my UITabBarController Custom Class I called this
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
// Allows scrolling to top on second tab bar click
if (viewController.isKindOfClass(CustomNavigationBarClass) && tabBarController.selectedIndex == 0) {
if (viewControllerRef!.canScrollToTop) {
viewControllerRef!.scrollToTop()
}
}
}
The Result is identical to Instagram and Twitter's feed :)

Swift 3 approach::
//MARK: Properties
var previousController: UIViewController?
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if self.previousController == viewController || self.previousController == nil {
// the same tab was tapped a second time
let nav = viewController as! UINavigationController
// if in first level of navigation (table view) then and only then scroll to top
if nav.viewControllers.count < 2 {
let tableCont = nav.topViewController as! UITableViewController
tableCont.tableView.setContentOffset(CGPoint(x: 0.0, y: -tableCont.tableView.contentInset.top), animated: true)
}
}
self.previousController = viewController;
return true
}
A few notes here::
"shouldSelect" instead of "didSelect" because the latter is taking place after transition, meaning viewController local var already changed.
2. We need to handle the event before changing controller, in order to have the information of navigation's view controllers regarding scrolling (or not) action.
Explanation:: We want to scroll to top, if current view is actually a List/Table view controller. If navigation has advanced and we tap same tab bar, desired action would be to just pop one step (default functionality) and not scroll to top. If navigation hasn't advanced meaning we are still in table/list controller then and only then we want to scroll to top when tapping again. (Same thing Facebook does when tapping "Feed" from a user's profile. It only goes back to feed without scrolling to top.

In this implementation you no need static variable and previous view controller state
If your UITableViewController in UINavigationController you can implement protocol and function:
protocol ScrollableToTop {
func scrollToTop()
}
extension UIScrollView {
func scrollToTop(_ animated: Bool) {
var topContentOffset: CGPoint
if #available(iOS 11.0, *) {
topContentOffset = CGPoint(x: -safeAreaInsets.left, y: -safeAreaInsets.top)
} else {
topContentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
}
setContentOffset(topContentOffset, animated: animated)
}
}
Then in your UITableViewController:
class MyTableViewController: UITableViewController: ScrollableToTop {
func scrollToTop() {
if isViewLoaded {
tableView.scrollToTop(true)
}
}
}
Then in UITabBarControllerDelegate:
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard tabBarController.selectedViewController === viewController else { return true }
guard let navigationController = viewController as? UINavigationController else {
assertionFailure()
return true
}
guard
navigationController.viewControllers.count <= 1,
let destinationViewController = navigationController.viewControllers.first as? ScrollableToTop
else {
return true
}
destinationViewController.scrollToTop()
return false
}
}

I have a collection view embedded in a navigation controller, in Swift this works.
var previousController: UIViewController?
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if previousController == viewController {
if let navVC = viewController as? UINavigationController, vc = navVC.viewControllers.first as? UICollectionViewController {
vc.collectionView?.setContentOffset(CGPointZero, animated: true)
}
}
previousController = viewController;
}

I've implemented a plug & play UITabBarController that you can freely re-use in your projects. To enable the scroll-to-top functionality, you should just have to use the subclass, nothing else.
Should work out of the box with Storyboards also.
Code:
/// A UITabBarController subclass that allows "scroll-to-top" gestures via tapping
/// tab bar items. You enable the functionality by simply subclassing.
class ScrollToTopTabBarController: UITabBarController, UITabBarControllerDelegate {
/// Determines whether the scrolling capability's enabled.
var scrollEnabled: Bool = true
private var previousIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
/*
Always call "super" if you're overriding this method in your subclass.
*/
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
guard scrollEnabled else {
return
}
guard let index = viewControllers?.indexOf(viewController) else {
return
}
if index == previousIndex {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), { [weak self] () in
guard let scrollView = self?.iterateThroughSubviews(self?.view) else {
return
}
dispatch_async(dispatch_get_main_queue(), {
scrollView.setContentOffset(CGPointZero, animated: true)
})
})
}
previousIndex = index
}
/*
Iterates through the view hierarchy in an attempt to locate a UIScrollView with "scrollsToTop" enabled.
Since the functionality relies on "scrollsToTop", it plugs easily into existing architectures - you can
control the behaviour by modifying "scrollsToTop" on your UIScrollViews.
*/
private func iterateThroughSubviews(parentView: UIView?) -> UIScrollView? {
guard let view = parentView else {
return nil
}
for subview in view.subviews {
if let scrollView = subview as? UIScrollView where scrollView.scrollsToTop == true {
return scrollView
}
if let scrollView = iterateThroughSubviews(subview) {
return scrollView
}
}
return nil
}
}
Edit (09.08.2016):
After attempting to compile with the default Release configuration (archiving) the compiler would not allow the possibility of creating a large number of closures that were captured in a recursive function, thus it would not compile. Changed out the code to return the first found UIScrollView with "scrollsToTop" set to true without using closures.

I tried the solution given by #jsanabria. This worked well on a fixed tableview, but it wouldn't work for my infinite scroll tableview. It only came up the table view about halfway after loading the new scrolling data.
Swift 5.0+
self.tableView.scrollToRow(at: IndexPath.init(row: 0, section: 0), at: UITableView.ScrollPosition(rawValue: 0)!, animated: true)

TESTED SOLUTION IN SWIFT
STEP 1
In your main tabbarcontroller class declare
weak static var previousController: UIViewController?
STEP 2
In viewdidLoad() set
MainTabBarViewController.previousController = viewControllers?[0]
STEP 3
extension MainTabBarViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if MainTabBarViewController.previousController == viewController {
/// here comes your code
}
MainTabBarViewController.previousController = viewController
}
}

I found the scrollRectToVisible method works better than the setContentOffset.
Swift:
After you catch the click on the tab bar from the delegate, something like below:
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if (viewController.isKindOfClass(SomeControllerClass) && tabBarController.selectedIndex == 0)
{
viewController.scrollToTop()
}
}
Now for the scrollToTop function inside the controller:
func scrollToTop()
{
self.tableView.scrollRectToVisible(CGRectMake(0,0,CGRectGetWidth(self.tableView.frame), CGRectGetHeight(self.tableView.frame)), animated: true)
}

Related

Tab bar click to scroll to top table view

I am using this View hierarchy.
UITabBarController > UINavigationController > UITableViewController -> UIViewController (On click of any table cell)
I want to scroll to top of the list on clicking the tab bar item that is currently selected.
How can I achieve this? Thanks in advance.
Add this to the target of your button
tableView.scrollsToTop = true
tableView.clipsToBounds = true
self.tableView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
First you need to declare a protocol
protocol RefreshProtocol {
func scrollToTopRefresh()
}
then what you need to do is to write delegate method of your custom tabBar Controller
class MainViewController: UITabBarController {}
extension MainViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if tabBarController.selectedViewController == viewController {
if let navController = viewController as? UINavigationController {
if let myViewController = navController.topViewController , let homeController = myViewController as? RefreshProtocol {
homeController.scrollToTopRefresh()
}
}
else {
if let homeController = viewController as? RefreshProtocol {
homeController.scrollToTopRefresh()
}
}
}
}
Whichever class confirm refresh protocol will implement this function like this
class HomeViewController:UIViewController{}
extension HomeViewController:RefreshProtocol {
func scrollToTopRefresh () {
let indexPath = IndexPath(row: 0, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
}
}

Trigger image picker on UITabBarController button click

I have made a tabbar in storyboard with 4 bar items, I connected all of them to other view controllers by rightclick dragging and setting viewcontroller segue. Now for the second button i want to show an imagepicker instead of a viewcontroller. When i delete the second segue from storyboard in UITabBarController, my 4th bar item disappears.
This is my tabview controller
class BaseTabBarController: UITabBarController, UITabBarControllerDelegate {
let arrayOfImageNameForUnselectedState = ["home", "explore", "addIcon", "notification", "accountIcon"]
let arrayOfImageNameForSelectedState = ["homeFilled", "exploreFilled", "addIcon", "notificaitonFilled", "accountIcon"]
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
if let count = self.tabBar.items?.count {
for i in 0..<count {
let imageNameForSelectedState = arrayOfImageNameForSelectedState[i]
let imageNameForUnselectedState = arrayOfImageNameForUnselectedState[i]
self.tabBar.items?[i].selectedImage = UIImage(named: imageNameForSelectedState)?.withRenderingMode(.alwaysOriginal)
self.tabBar.items?[i].image = UIImage(named: imageNameForUnselectedState)?.withRenderingMode(.alwaysOriginal)
}
}
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if tabBar.items?.index(of: item) ?? 0 == 2 {
//Clicked add tab, cancel segue and show imagepicker
} else {
selectedTabindex = tabBar.items?.index(of: item) ?? 0
}
}
}
How do I show image picker on 2nd bar button click
Do not Delete Second segue from storyboard put dummy view controller to show tab button inside tabbar. Implement UITabBarController controller's delegate method in subclass of UITabBarController and return false in shouldSelect method for second viewcontroller and present ImagePicker View for there.
Code:
class BaseTabBarController : UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
override var prefersStatusBarHidden: Bool {
return false
}
}
extension BaseTabBarController : UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if self.viewControllers?.index(of:viewController) == 1 {
// TO Do code for Image Picker and Present it
return false
} else {
return true
}
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
}
}

UITabBarControllerDelegate on two separate UIViewControllers - Swift

I have the following code that i'm trying to achieve a "scroll to top" on a UIViewcontroller and "scroll to beginning" on another UIViewController.
The first VC has a table view and the second VC has a collection view.
-NearMeViewController-
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
//....
}
extension NearMeViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
self.nearMeTable.setContentOffset(CGPoint.zero, animated: true)
}
}
}
-MapSearchViewController-
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
//....
}
extension MapSearchViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 2 && requests.mapEventData.count > 0 {
self.resultsCollectionView?.scrollToItem(at: IndexPath(row: 0, section: 0),
at: .left,
animated: true)
}
}
}
They are both connected to the same UITabBar of course.
The issue is that when i open the app, and i press the tabBarIndex 0 it scrolls to top of the table view.
Then i change VC and i try to press the tabBarIndex 2, so it takes me to the first item of the collection view.
Afterwards I go back to the first VC and when i try to press the tabBarIndex 0, the tableview is not scrolling to top (as it was doing when i first opened the app).
But the second VC with the collection view is working fine.
Any idea why this might happen?
viewDidLoad is not being called each time the VC appears but only once (or when it was deallocated before)
the delegate of vc 0 is set only once... and then vc1 is loaded and set as delegate... but then not vc0 again.
to make it work, set the delegate in viewWillAppear.
note
changing the delegate over and over for this is weird... and checking the index is fragile at best go with DonMag's solution.
It looks like you are checking the tabBarController's .selectedIndex ... but that is the index of the currently showing tab, not the tab you are "on your way to."
More likely, you want to use code similar to this:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if let vc = viewController as? MapSearchViewController {
// user tapped Map Search tab item
}
if let vc = viewController as? NearMeViewController {
// user tapped Near Me tab item
}
}
Edit: Here is a simple example, using a subClassed UITabBarController.
Set up your storyboard as normal, creating View Controllers and connecting them as "Tabs", then set the Custom Class of the TabBarController to MyTabBarController. This sample code will (hopefully) be easy to follow.
A runnable example is at: https://github.com/DonMag/SWTabBarSubclass
//
// TabBarSample.swift
//
import UIKit
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// all your normal setup stuff....
}
func firstViewSpecific(_ aValue: Int) -> Void {
print("Passed value is:", aValue)
// do something with the passed value
}
}
class NearMeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// all your normal setup stuff....
}
func resetMe() -> Void {
print("resetMe() called in NearMeVC")
// do whatever you want, such as
self.nearMeTable.setContentOffset(CGPoint.zero, animated: true)
}
}
class MapSearchViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// all your normal setup stuff....
}
func resetMe() -> Void {
print("resetMe() called in MapSearchVC")
// do whatever you want, such as
if requests.mapEventData.count > 0 {
self.resultsCollectionView?.scrollToItem(at: IndexPath(row: 0, section: 0),
at: .left,
animated: true)
}
}
}
class MyTabBarController: UITabBarController, UITabBarControllerDelegate {
var myValue = 0
override func viewDidLoad() {
super.viewDidLoad()
// make self the UITabBarControllerDelegate
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if let vc = viewController as? FirstViewController {
myValue += 1
vc.firstViewSpecific(myValue)
return
}
if let vc = viewController as? NearMeViewController {
vc.resetMe()
return
}
if let vc = viewController as? MapSearchViewController {
vc.resetMe()
return
}
}
}
might not apply to all scenarios, check if your VC has been set to
self.tabBarController?.delegate = self
within your ViewDidLoad()

Tap UITabBarItem To Scroll To Top Like Instagram and Twitter

I'm having issues making this feature work and i would like to get some help.
My hierarchy is TabBarController -> Navigation Controller -> TableViewController
What i want is if you are on current tab and you scrolled down you will be able to tap the current View's UITabBarItem and you will be scrolled back to the top,Like Instagram and Twitter does for example.
I have tried many things right here :
Older Question
but sadly non of the answers did the job for me.
I would really appreciate any help about this manner ,
Thank you in advance!
Here is my TableView`controller's Code :
import UIKit
class BarsViewController: UITableViewController,UISearchResultsUpdating,UISearchBarDelegate,UISearchDisplayDelegate,UITabBarControllerDelegate{
//TableView Data & non related stuff....
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.searchController.searchBar.resignFirstResponder()
self.searchController.searchBar.endEditing(true)
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
let indexPath = IndexPath(row: 0, section: 0)
let navigVC = viewController as? UINavigationController
let finalVC = navigVC?.viewControllers[0] as? BarsViewController
finalVC?.tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
}
}
}
TabBarController.Swift Code ( Code doesn't work ) :
import UIKit
class TabBarController: UITabBarController,UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? UINavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
var isScrolledToTop: Bool {
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
Here you go, this should work:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? ZBNavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
and then...
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
// Changed this
var isScrolledToTop: Bool {
if self is UITableViewController {
return (self as! UITableViewController).tableView.contentOffset.y == 0
}
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
There's a bit extra in this function so that if the UIViewController is already at the top it will pop to the previous controller
Try this code in your TabViewController:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
let indexPath = NSIndexPath(row: 0, section: 0)
let navigVC = viewController as? UINavigationController
let finalVC = navigVC?.viewControllers[0] as? YourVC
finalVC?.tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
}
}
Also, your TabViewController should inherit from UITabBarControllerDelegate
final code:
import UIKit
class tabViewController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
let indexPath = NSIndexPath(row: 0, section: 0)
let navigVC = viewController as? UINavigationController
let finalVC = navigVC?.viewControllers[0] as? YourVC
finalVC?.tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
}
}
}
Remember to change tabBarIndex and set self.delegate = self in viewDidLoad
You have just to create a file TabBarViewController with this code :
class TabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? UINavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
var isScrolledToTop: Bool {
if self is UITableViewController {
return (self as! UITableViewController).tableView.contentOffset.y == 0
}
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
And then in your storyboard, set custom class TabBarController like this :
Great examples! I've tried some things also and I guess this small piece of code for our delegate is all we need:
extension AppDelegate: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// Scroll to top when corresponding view controller was already selected and no other viewcontrollers are pushed
guard tabBarController.viewControllers?[tabBarController.selectedIndex] == viewController,
let navigationController = viewController as? UINavigationController, navigationController.viewControllers.count == 1,
let topViewController = navigationController.viewControllers.first,
let scrollView = topViewController.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView else {
return true
}
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: true)
return false
}
}
I just had to implement this and was surprised to find out it wasn't as easy as I thought it would be. This is how I implemented it:
A few notes on my application's setup
The MainTabBarcontroller is our root view controller and we generally access it from the UISharedApplication.
Our navigation structure is MainTabBarController -> Nav Controller -> Visible View Controller
The main issue I found with implementing this is I only wanted to scroll to the top if I was already on the first screen in the tab bar but once you try to make this check from tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) you've already lost a reference to the view controller that was visible before you tapped it. It is also a problem to compare view controllers when you're switching tabs since the visible view controller might be in another tab.
So I created a property in my TabBar class to hold a reference to the previousTopVC and created a protocol to help me set this property from the currently visible VC.
protocol TopScreenFindable {
func setVisibleViewController()
}
extension TopScreenFindable where Self: UIViewController {
func setVisibleViewController() {
guard let tabController = UIApplication.shared.keyWindow?.rootViewController as? MainTabBarController else { return }
tabController.previousTopVC = self
}
}
I then called conformed to this protocol and setVisibleViewController from the View Controllers' viewDidAppear and now had a reference to the previous visible VC when any screen showed up.
I created a delegate for my MainTabBarController class
protocol MainTabBarDelegate: class {
func firstScreenShouldScrollToTop()
}
Then called it from the UITabBarControllerDelegate method
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard let navController = viewController as? UINavigationController, let firstVC = navController.viewControllers.first, let previousVC = previousTopController else { return }
if firstVC == previousVC {
mainTabBarDelegate?.firstScreenShouldScrollToTop()
}
}
And in the First View Controllers conformed to the MainTabBarDelegate
extension FirstViewController: MainTabBarDelegate {
func firstScreenShouldScrollToTop() {
collectionView.setContentOffset(CGPoint(x: 0, y: collectionView.contentInset.top), animated: true)
}
I set the tabBar.mainTabBarDelegate = self on the FirstViewController's viewDidAppear so that the delegate was set every time the screen showed up. If I did it on viewDidLoad it wouldn't work when switching tabs.
A couple things I didn't like about my approach.
Having a reference to the tab bar in my view controllers just so it can set itself as its delegate.
Making every relevant screen in the app conform to the TopScreenFindable protocol

Determine viewWillAppear from Popped UINavigationController or UITabBarController

I am unable to find a way to distinguish between popping from the Nav controller stack and entering the view controller from the UITabBarController.
I want to call a method in ViewWillAppear only when the view is presented from the TabBar, not when someone presses back in the navigation controller.
If I wasn't using a TabBarController, I could easily get this functionally using viewDidLoad.
I've tried,
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
println("View Will Appear")
if isBeingPresented() {
println("BP")
}
if isMovingFromParentViewController() {
println("from")
}
if isMovingToParentViewController() {
println("to")
}
}
But there is no difference when I present from pressing the Tab Button or when press back button.
Only the "View Will Appear" is getting called.
Using iOS 8.4 / Swift
Sounds like a good use of the UITabBarControllerDelegate.
First, add a Bool property on your ViewController comingFromTab:
class MyViewController: UIViewController {
var comingFromTab = false
// ...
}
Set your UITabBarControllerDelegate to whatever class you want and implement the method shouldSelectViewController. You may also want to subclass UITabBarController and put them in there.
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
if let myViewController = viewController as? MyViewController {
myViewController.comingFromTab = true
}
If your tab's initial view controller is a UINavigationController, you will have to unwrap that and access it's first view controller:
if let navController = viewController as? UINavigationController {
if let myViewController = navController.viewControllers[0] as? MyViewController {
// do stuff
}
}
Lastly, add whatever functionality you need in viewWillAppear in your view controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// ...
if comingFromTab {
// Do whatever you need to do here if coming from the tab selection
comingFromTab = false
}
}
There is no way to know for sure. So I guess the easiest way is to add some variable that you will have to change before popping back to that view controller and checking it's state in viewWillAppear.
class YourViewController: UIViewController {
var poppingBack = false
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if !poppingBack {
// your logic
}
else {
poppingBack = false // reset it for next time
}
}
}
// somewhere else in code, suppose yourVC is YourViewController
yourVC.poppingBack = true
self.navigationController.popToViewController(yourVC, animated: true)
You can also try implementing UINavigationControllerDelegate's - navigationController:willShowViewController:animated: method and check if it will be called when presenting your view controller from tab bar.
You can check parentViewController property
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if parentViewController is UITabBarController {
// Presented by UITabBarController
} else if parentViewController is UINavigationController {
// Presented by UINavigationController
} else {
// Presented by ...
}
}

Resources