Swift language
Main issue
In the image I have a green and a blue view. I am able to swipe between those two views and now I want to implement a UITabBar so I know on which view I selected. I tried UIPageControlbut I can not customize the indicators in any way. I tried creating a UITabBarController but I do not know where to put it.
First attempt
If I connect the UITabBarController with the green and blue view using segues then the indicator will not load, just the bar.
Second attempt
If I connect the UITabBarController with the pageviewcontroller or the container view it will add them into the indicator and when I connect the blue and green view after I will not be able to slide.
Last attempt(not tried it)
The only thing I have not tried is doing it programmatically inside the pageviewcontrollers.swift file. I do not really know how to implement a TabBar programmatically and I would like to ask for some help. If you want to know why I would rather have a UITabBar instead of page control indicator is because I can edit the indicators to icons instead.
Code in pageviewcontroller
import UIKit
class secondPageViewController: UIPageViewController , UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var pages = [UIViewController]()
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.dataSource = self
let page4: UIViewController! = storyboard?.instantiateViewControllerWithIdentifier("page4")
let page5: UIViewController! = storyboard?.instantiateViewControllerWithIdentifier("page5")
pages.append(page4)
pages.append(page5)
setViewControllers([page4], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.indexOf(viewController)!
let previousIndex = abs((currentIndex - 1) % pages.count)
if (previousIndex > 0)
{
return nil
}
return pages[previousIndex]
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.indexOf(viewController)!
let nextIndex = abs((currentIndex + 1) % pages.count)
if (nextIndex < 1)
{
return nil
}
return pages[nextIndex]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Link to answer: https://github.com/uacaps/PageMenu
Fully functional PageMenu with options.
Related
My problem is as follows: I am using pageViewControllers to create a tutorial/onboarding page for an app i'm creating. A left swipe gesture on the last (3rd) page of my tutorial should perform a segue.
This is what I have as of now, but it is giving me a key value coding-compliancy error. I have double, triple checked my outlets, and swipeView is very much so connected properly.
class GrowController: UIViewController {
#IBOutlet weak var swipeView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(swipeView)
createGesture()
}
func createGesture() {
let showReg = UISwipeGestureRecognizer(target: self, action: #selector(showRegister))
showReg.direction = .left
self.swipeView.addGestureRecognizer(showReg)
}
func showRegister(gesture: UISwipeGestureRecognizer) {
performSegue(withIdentifier: "showRegister", sender: self)
}
}
That is the controller for the actual UIViewController (the specific page that is being displayed)
now ive also tried messing around with some logic in my TutorialViewController which is the UIPageViewController controlling the swipes form page to page etc.
Logic for that here
class TutorialViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var viewControllerIndex: Int?
//Array of my pages to load (GrowController is page3)
lazy var tutorialArray: [UIViewController] = {
return [self.tutorialInstance(name: "page1"), self.tutorialInstance(name: "page2"), self.tutorialInstance(name: "page3")]
}()
private func tutorialInstance(name: String?) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: name!)
}
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
if let firstViewController = tutorialArray.first {
setViewControllers([firstViewController], direction: .forward, animated: false, completion: nil)
}
}
// Scroll view
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews {
if view is UIScrollView {
view.frame = UIScreen.main.bounds
}
else if view is UIPageControl {
view.backgroundColor = UIColor.clear
}
}
}
// Page View Controller delegate functions
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = tutorialArray.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
guard tutorialArray.count > previousIndex else {
// Added this line just testing around, nothing happened here though.
performSegue(withIdentifier: "ShowRegister", sender: self)
return nil
}
return tutorialArray[previousIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = tutorialArray.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard nextIndex < tutorialArray.count else {
return nil
}
guard tutorialArray.count > nextIndex else {
return nil
}
return tutorialArray[nextIndex]
}
public func presentationCount(for pageViewController: UIPageViewController) -> Int {
return tutorialArray.count
}
public func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first, let firstViewControllerIndex = tutorialArray.index(of: firstViewController) else {
return 0
}
return firstViewControllerIndex
}
}
Does is have something to do with the willTransitionTo pageView delegate method? I'm not familiar how to implement that, I have tried.
I thought that i could just add a subView to the Grow controller, put a swipe gesture in it, and perform a segue whenever the user swipes left from that page. As of right now, this code crashes the app upon loading of page3 (GrowController)
Any help GREATLY appreciated, I've been trying to figure this out for over a week and this is the second question on the topic i've posed. Thanks!
Don't use a UIPageViewController for this. That's very ironic, because I am usually the first to advise using UIPageViewController. But in this case it is doing too much of the work for you, and you cannot interfere in such a way as to customize it the way you're describing. You will need to use a simple paging UIScrollView; that way, you will be totally in charge of what happens, with fine-grained response thanks to the scroll view's delegate, and you'll have access to the underlying pan gesture recognizer and can modify its behavior.
Double tapping anywhere at the bottom of a page control area (where the dots are) pauses the transition from one view controller to another.
Example of double tapping:
I simplified a UIPageViewController project to demonstrate this:
Storyboard
PageViewController.swift
import UIKit
class PageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var pages: [UIViewController] = [UIViewController]()
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
pages.append(UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Page1"))
pages.append(UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Page2"))
setViewControllers([pages.first!], direction: .forward, animated: true, completion: nil)
}
// This allows the dots to appear on top of the views
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews {
if view is UIScrollView {
view.frame = UIScreen.main.bounds
} else if view is UIPageControl {
view.backgroundColor = UIColor.clear
}
}
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let viewControllerIndex = pages.index(of: viewController)
if viewControllerIndex == 1 {
return pages[0]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let viewControllerIndex = pages.index(of: viewController)
if viewControllerIndex == 0 {
return pages[1]
}
return nil
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
}
The source of the problem lies in this code:
// This allows the dots to appear on top of the views
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews {
if view is UIScrollView {
view.frame = UIScreen.main.bounds
} else if view is UIPageControl {
view.backgroundColor = UIColor.clear
}
}
When this code is removed there is no partial transitions but you will now see a black bar which is also not desired.
This is a common solution I have found throughout the web and Stackoverflow to get the Page Control (dots) to show on top of the views instead of within its own bar at the bottom of the screen.
So that's all I'm looking for. A solution that:
Shows the dots on top of the views (no black bar)
No problems with transitioning.
Thanks in advance for any help!
There is very nice tutorial How to Use UIPageViewController in Swift which explains how to configure UIPageViewController
Second part of this tutorial How to Move Page Dots in a UIPageViewController which i believe is answer to your question.
You can use Container View to embed UIPageViewController in it and then you can keep any view top of the page view controller which does not scroll with UIPageViewController
I hope this helps.
Thanks, Jay.
For me the container view was the simplest solution. What I did was cover up the Page Control at the bottom of the page (dots) with a UIView that had the background color set to clear color. So I'm preventing the user from double tapping in that area. It totally feels like a hack but I honestly didn't want to implement all that custom code in that second link you posted.
Then I went to https://bugreport.apple.com and requested an Xcode Enhancement for the UIPageViewController that gives developers a checkbox to overlay the page control over the view or use the black bar. :D
Jay, thanks again for helping me on this. I didn't know you could reference the UIPageControl in the UIPageViewController but I did some research and found a way. I changed the viewDidLoad to this and it worked! No hack needed:
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
setViewControllers([pages.first!], direction: .forward, animated: true, completion: nil)
let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [PageViewController.self])
pageControl.isUserInteractionEnabled = false
}
Courtesy of Using UIPageViewController with swift and multiple view controllers, I'm embedding navigation controllers in a UIPageViewController so I can horizontally scroll thru them (like swipe nav). Problem:
When I reach the first or last nav controller, and then swipe in the opposite direction, the 2nd-to-first/last nav controller will duplicate. So I'll have 5 nav controllers. For example:
Starting at FirstNav, swipe left all the way to FourthNav. When I swipe right through the array of controllers from FourthNav, the sequence will be: ThirdNav, ThirdNav, SecondNav, FirstNav. Can anyone find out what's going on?
class PageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var index = 0
var identifiers: NSArray = ["FirstNav", "SecondNav", "ThirdNav", "FourthNav"]
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
let startingViewController = self.viewControllerAtIndex(self.index)
let viewControllers: NSArray = [startingViewController]
self.setViewControllers(viewControllers as [AnyObject], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
func viewControllerAtIndex(index: Int) -> UINavigationController! {
if index == 0 {
return self.storyboard!.instantiateViewControllerWithIdentifier("FirstNav") as! UINavigationController
}
if index == 1 {
return self.storyboard!.instantiateViewControllerWithIdentifier("SecondNav") as! UINavigationController
}
if index == 2 {
return self.storyboard!.instantiateViewControllerWithIdentifier("ThirdNav") as! UINavigationController
}
if index == 3 {
return self.storyboard!.instantiateViewControllerWithIdentifier("FourthNav") as! UINavigationController
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
let index = self.identifiers.indexOfObject(identifier!)
//if the index is the end of the array, return nil since we dont want a view controller after the last one
if index == identifiers.count - 1 {
return nil
}
//increment the index to get the viewController after the current index
self.index = self.index + 1
return self.viewControllerAtIndex(self.index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
let index = self.identifiers.indexOfObject(identifier!)
//if the index is 0, return nil since we dont want a view controller before the first one
if index == 0 {
return nil
}
//decrement the index to get the viewController before the current one
self.index = self.index - 1
return self.viewControllerAtIndex(self.index)
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
}
The issue has to do with your calculation of the index variable in your pageViewController(_:viewControllerBeforeViewController:) and pageViewController(_:viewControllerAfterViewController:) methods. To simplify that logic and the overall logic of your page view controller, you should change your implementation to the following:
class PageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
private var pages: [UIViewController]!
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
self.pages = [
self.storyboard!.instantiateViewControllerWithIdentifier("FirstNav") as! UINavigationController,
self.storyboard!.instantiateViewControllerWithIdentifier("SecondNav") as! UINavigationController,
self.storyboard!.instantiateViewControllerWithIdentifier("ThirdNav") as! UINavigationController,
self.storyboard!.instantiateViewControllerWithIdentifier("FourthNav") as! UINavigationController
]
let startingViewController = self.pages.first! as UIViewController
self.setViewControllers([startingViewController], direction: .Forward, animated: false, completion: nil)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let index = (self.pages as NSArray).indexOfObject(viewController)
// if currently displaying last view controller, return nil to indicate that there is no next view controller
return (index == self.pages.count - 1 ? nil : self.pages[index + 1])
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let index = (self.pages as NSArray).indexOfObject(viewController)
// if currently displaying first view controller, return nil to indicate that there is no previous view controller
return (index == 0 ? nil : self.pages[index - 1])
}
}
You don't even need to maintain an index instance variable, and this implementation does not do so. But you could, if you wanted. This solution also instantiates a single instance of each UINavigationController instead of instantiating a new one every time the user attempts to scroll to a different page, which conserves memory and preserves the state of the view controllers as the user scrolls between them.
Please excuse the non-descriptive pages variable name. I didn't want to create a conflict with UIPageViewController's viewControllers property.
I am developing an application for iOS using swift.
I have already set up one UIPageViewController successfully in my project, so I know how it works.
But now I'm trying to set up a second PageViewController, and it just doesn't work - it doesn't throw any errors either, it just goes directly to black screen and doesn't even show any segues
What I have tried so far:
Resetting the iOS simulator
Reindexing the xcode files
Restarting xcode
Restarting my machine
None of these things have helped.
My code:
(BTW: This code is almost the exactly same as in my working UIPageViewController, the only difference is in the variables' names)
import UIKit
class SelectDestinationViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var destinationViewControllers : [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
println("View did appear")
// Do any additional setup after loading the view.
self.delegate = self
self.dataSource = self
let firstView = storyboard?.instantiateViewControllerWithIdentifier("firstView") as FirstDestinationViewController
let secondView = storyboard?.instantiateViewControllerWithIdentifier("secondView") as SecondDestinationViewController
destinationViewControllers = [firstView, secondView]
for var index = 0; index < destinationViewControllers.count; ++index {
println("\(destinationViewControllers[index])")
}
let startingViewController = self.viewControllerAtIndex(0)
let viewControllers: NSArray = [startingViewController]
self.setViewControllers(viewControllers, direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: {(done: Bool) in})
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
println("View will appear!")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func viewControllerAtIndex(index:NSInteger) -> UIViewController {
return destinationViewControllers[index]
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var index = find(destinationViewControllers, viewController)!
if (index == 0) || (index == NSNotFound) {
return nil
}
index--
if index == destinationViewControllers.count {
return nil
}
return self.viewControllerAtIndex(index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var index = find(destinationViewControllers, viewController)!
if index == NSNotFound {
return nil
}
index++
if index == destinationViewControllers.count {
return nil
}
return self.viewControllerAtIndex(index)
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return destinationViewControllers.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
}
All the prints are showing up in the console - but nothing else is happening.
Does anyone know what the problem might be?
Thank you in advance :)
Believe it or not the UIPageViewController class is actually not supposed to be viewed by itself. As with the split view controller class (but unlike the tab controller class) you need to have a master view which wraps up the page view controller.
You also need to do some inelegant tricks to get the page view controllers to load without being linked in via the storyboard. When you see code where someone has got it working you can tell they had to know way too much about UIViewController internals to get something so simple working..
It would be a bit clearer if, as with the split view controller, it showed you a standard set-up when you put it into your storyboard, but unfortunately the tab view, split view, and page view all behave very differently.
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)
}