Tab bar click to scroll to top table view - ios

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)
}
}
}
}

Related

How can I bring a collectionView back to the top when the tabBar icon for that respective view controller is pressed?

I am trying to build a social media app in swift that has a tab bar with 4 icons each connected to a view controller. I want to have the four collection views (that are each associated with a view controller) scroll back to the top when the respective tab bar icon is pressed (Exactly like twitter).
I have the logic for how I want the collection view to come back to the top only if the past tab bar icon was for that respective collection view (this is so that it doesn't just automatically scroll to the top when clicking on the respective tab but needs to be pressed again) I tried to debug my first switch case but have had no luck so I do not have the full code for the other cases yet. What I try to do is reference the correct storyboard, then the correct view controller (in this case homeViewController), and finally call that respective collection view and use setContentOffset to bring it to the top.
Below I provide my whole tabBarController class.
import Foundation
import UIKit
class MainTabController: UITabBarController, UITabBarControllerDelegate {
var pastTabBar: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let tabBarIndex = tabBar.items?.index(of: item)
if tabBarIndex == pastTabBar {
switch tabBarIndex {
case 0:
print ("go to top of home")
//updateHomeCollection = 1
let HomeSB : UIStoryboard = UIStoryboard(name: "Home", bundle: nil)
let HomeVC = HomeSB.instantiateViewController(withIdentifier: "Home") as? HomeViewController
HomeVC?.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
case 1:
print ("go to top of search")
case 2:
print ("go to top of notifications")
case 3:
print ("go to top of profile")
default:
print ("not working")
}
}
if let tabBarIndex = tabBarIndex {
pastTabBar = tabBarIndex
}
}
}
It keeps signal aborting when I click the home tab! It is printing this exact statement:
Thread 1: Fatal error: Unexpectedly found nil while implicitly
unwrapping an Optional value
on this line of code:
HomeVC?.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
Loading ViewController is not best way. You have to fetch your desired ViewController from Navigation like below
extension HomeTabVC: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if previousController == viewController {
if let nav = viewController as? UINavigationController, let vc = nav.viewControllers[0] as? FeaturedVC {
if vc.isViewLoaded && (vc.view.window != nil) {
let visibleIndex = vc.collectionFeatured.indexPathsForVisibleItems
if visibleIndex.count != 0 {
vc.collectionFeatured.scrollToItem(at: IndexPath (item: 0, section: 0), at: .bottom, animated: true)
}
}
}else if let nav = viewController as? UINavigationController, let vc = nav.viewControllers[0] as? CategoryVC {
if vc.isViewLoaded && (vc.view.window != nil) {
let visibleIndex = vc.collectionViewCategory.indexPathsForVisibleItems
if visibleIndex.count != 0 {
vc.collectionViewCategory.scrollToItem(at: IndexPath (item: 0, section: 0), at: .bottom, animated: true)
}
}
}else{
}
}
previousController = viewController
}
}
You have to store Selected ViewController in variable so when you change ViewController from one tab to another it will just change ViewController and when you tap same tab again it will scroll you back to top. This is how iOS is doing it self in all there application

swift4: how to check if user logged befor tab bar item clicked

This question has been asked before and has been answered, my question is not unique but there must be something missing. I'm simply trying to check if user logged in to app before as his data stored in UserDefaults but it doesn't work for me, this is the class of my TabBarViewController
class TabViewController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
// UITabBarDelegate
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
print("Selected item")
}
// UITabBarControllerDelegate
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if(viewController is MessagesViewController) {
print("trueee")
if(UserStorage.id == "") {
Toast.toast(messsage: "not loggoed user", view: self.view)
}
} else {
print("faaaaaaaalse")
}
print("Selected view controller")
}
}
i want to check if user open MessageViewController then if user is logged in to print something but it always print faaaaaaaalse
note: Toast.toast() is a function i created to show toast
and UserStorage.id returns user id stored in USerDefaults
this is image which shows my structure:
what should I do ?
Just your tab bar’s root controllers are 2 navigaton controllers, not MessageViewController. Firstly with tabBarController you have to find navigationController which contains your MessageViewController than in this navigation find the needed ViewController.
So I have the solution for you:
import UIKit
class TabbarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
// for tab bar initialization
if let viewControllers = self.viewControllers,
viewControllers.count >= 1,
// the index of viewController is 0 here, but if your tab bar's started controller is not 0 you can set yours
let navigationController = viewControllers[0] as? UINavigationController {
for controller in navigationController.viewControllers {
if let messagesViewController = controller as? MessagesViewController {
doWithMessagesViewControllerWhatYouWant(_viewController: messagesViewController)
}
}
}
}
// UITabBarControllerDelegate
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if let navigationController = viewController as? UINavigationController{
for controller in navigationController.viewControllers {
if let messagesViewController = controller as? MessagesViewController {
doWithMessagesViewControllerWhatYouWant(_viewController: messagesViewController)
}
}
}
}
private func doWithMessagesViewControllerWhatYouWant(_viewController: MessagesViewController) {
print("do some operations with messagesViewController")
if(UserStorage.id == "") {
Toast.toast(messsage: "not loggoed user", view: self.view)
}
}
}

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

Tap tab bar to scroll to top of UITableViewController

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)
}

Resources