5

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
    }

}
Community
  • 1
  • 1
Newbie Questions
  • 463
  • 1
  • 11
  • 31

5 Answers5

4

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

nickromano
  • 918
  • 8
  • 16
jackchmbrln
  • 1,672
  • 2
  • 14
  • 26
  • What do you mean it will "pop" to the previous controller? and do i put this code in my `tableViewController` or `TabBarController`? – Newbie Questions Apr 14 '17 at 13:50
  • In your UITabBarController. But make sure to put the `UIViewController` extension outside of a class. Popping just means that if there is a previous view controller in the navigation stack it will slide back to it, probably not necessary but a nice little addition – jackchmbrln Apr 14 '17 at 13:51
  • Ah i see , really nice . I'm trying your answer now. – Newbie Questions Apr 14 '17 at 13:53
  • I'm getting an error saying: "`Expected declaration`" in the last `return true` of the func : `func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {` – Newbie Questions Apr 14 '17 at 13:57
  • You probably have an extra `{` or `}` somewhere – jackchmbrln Apr 14 '17 at 13:58
  • fixed it ... instead of 3 - `}` between the two return true there should be 2 and another - `}` after the return true. Running and testing now . – Newbie Questions Apr 14 '17 at 14:01
  • Ah, my bad. I removed an extra `if` condition for the answer so just remove the last `}` before `return true` – jackchmbrln Apr 14 '17 at 14:01
  • sadly the code isn't working for me....i will upload code to question to show you what i have. – Newbie Questions Apr 14 '17 at 14:03
3

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

enter image description here

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

Phyber
  • 1,368
  • 11
  • 25
3

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 :

enter image description here

BSK-Team
  • 1,750
  • 1
  • 19
  • 37
2

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
}

}

Reinier
  • 21
  • 1
0

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
EcuaCode
  • 11
  • 5