I'd like to implement a custom transition to ONE specific view-controller inside my UINavigationController.
I can do so by providing my custom TransitionAnimator inside
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
or return nil otherwise and everything works fine! So the animation part is solved and working!
BUT by returning nil for the times I want the default transition, I loose the back-swipe gesture.
So my question is:
How can I provide my custom transition to one specific VC, but keep everything else as is?!?
You can try to subclass UINavigationController and manage interactivePopGestureRecognizer on your own.
class CustomNavController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
}
extension CustomNavController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === interactivePopGestureRecognizer {
return viewControllers.count > 1
}
return true
}
}
You could make the view controller the navigation delegate and then reset it back to nil. like:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.interactivePopGestureRecognizer?.delegate = self
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
navigationController?.interactivePopGestureRecognizer?.delegate = nil
}
Then perform your custom animation in the view controller or make an object/class to handle it for you
I have an UITabBarController and sometimes in didSelectItem delegate I need to pause the event and present a popup. If user confirmed the event resumes and if not, event will be canceled. Here's my code:
class YC_TabBarController: UITabBarController {
var prevIndex: Int!
var exitAction: (()->Bool)?
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
self.prevIndex = self.selectedIndex
if self.prevIndex == 2 {
guard self.exitAction != nil else {return}
//pause
let isExitAccepted: Bool = self.exitAction!()
//if true -> resume
//if false -> prevent from switching tab
}
}
}
How can I do that? Please Help
You should confirm to UITabBarControllerDelegate in first view controller and return false if the desired view controller is selected in shouldSelect viewController. Then you should show your popup view. In popup view ok/confirm button you can change selected view controller of the self.tabBarController
class ViewController: UIViewController,UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController is SecondViewController {
//show alert
return false
} else {
return true
}
}
func popUpOkAction(_ sender:UIButton) {
if let secVC = self.tabBarController?.viewControllers?.first(where: { $0 is SecondViewController }) {
self.tabBarController?.selectedViewController = secVC
}
}
}
If you want to perform this from multiple view controllers rather than
firstViewController you can confirm to UITabBarControllerDelegate in
YC_TabBarController itself.
In one of my iOS project, I have added a subview for Filter ViewController(subview) on my Feed ViewController(main view) programmatically.
There are few button on Filter ViewController to select price, city etc.
The outlets are connected but when I am trying to shoot button action, it is not working.
I have also enabled the isUserInteractionEnabled but still it is not working.
Acc. to me, this is something related to subview on a view !! but to resolve this. Can you suggest me how to shoot a button action of subview it happen ?
class FilterViewController: BaseUIViewController{
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
cityButton.isUserInteractionEnabled = true
}
#IBAction func selectCity(_ sender: Any)
{
print("selectCity action")
}
}
Add your FilterViewController as subview to your ViewController using the code below
filterViewController.willMove(toParentViewController: self)
self.view.addSubview(filterViewController.view)
self.addChildViewController(filterViewController)
filterViewController.didMove(toParentViewController: self)
#SandeepBhandari solution is still valid for similar issues.
These UIViewController extensions will be useful.
extension UIViewController {
func add(asChildViewController viewController: UIViewController, containerView: UIView) {
addChild(viewController)
containerView.addSubview(viewController.view)
viewController.view.frame = containerView.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
viewController.didMove(toParent: self)
}
func remove(asChildViewController viewController: UIViewController) {
viewController.willMove(toParent: nil)
viewController.view.removeFromSuperview()
viewController.removeFromParent()
}
}
How do I determine when the parent view controller has been hidden or shown when I use the UIModalPresentationOverCurrentContext modal presentation style? On normal situations I can use the viewWillAppear: and viewWillDisappear:, but they seem not to be firing on this.
UIModalPresentationOverCurrentContext is intended to be used to present the content over your current viewController. What that means is, if you have animation or view changes inside your parentViewController, you can still see through the childViewController if the view is transparent. So, it also means that view never disappears for view over current context. It seems legit that viewWillAppear:, viewDidAppear:, viewWillDisappear: and viewDidDisappear do not get called.
You can however use UIModalPresentationStyle.Custom to have exact same behavior to present over current context. You wont receive view appearance callbacks but you can create your own custom UIPresentationController to get those callbacks.
Here is an example implementation,
class MyFirstViewController: UIViewController {
....
func presentNextViewController() {
let myNextViewController = MyNextViewController()
myNextViewController.modalPresentationStyle = UIModalPresentationStyle.Custom
myNextViewController.transitioningDelegate = self
presentViewController(myNextViewController, animated: true) { _ in
}
}
...
}
extension MyFirstViewController: UIViewControllerTransitioningDelegate {
func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController?
{
let customPresentationController = MyCustomPresentationController(presentedViewController: presented, presentingViewController: presenting)
customPresentationController.appearanceDelegate = self
return customPresentationController
}
}
extension MyFirstViewController: MyCustomApprearanceDelegate {
func customPresentationTransitionWillBegin() {
print("presentationWillBegin")
}
func customPresentationTransitionDidEnd() {
print("presentationDidEnd")
}
func customPresentationDismissalWillBegin() {
print("dismissalWillBegin")
}
func customPresentationDismissalDidEnd() {
print("dismissalDidEnd")
}
}
protocol MyCustomApprearanceDelegate: class {
func customPresentationTransitionWillBegin()
func customPresentationTransitionDidEnd()
func customPresentationDismissalWillBegin()
func customPresentationDismissalDidEnd()
}
class MyCustomPresentationController: UIPresentationController {
weak var appearanceDelegate: MyCustomApprearanceDelegate!
override init(presentedViewController: UIViewController, presentingViewController: UIViewController) {
super.init(presentedViewController: presentedViewController, presentingViewController: presentingViewController)
}
override func presentationTransitionWillBegin() {
appearanceDelegate.customPresentationTransitionWillBegin()
}
override func presentationTransitionDidEnd(completed: Bool) {
appearanceDelegate.customPresentationTransitionDidEnd()
}
override func dismissalTransitionWillBegin() {
appearanceDelegate.customPresentationDismissalWillBegin()
}
override func dismissalTransitionDidEnd(completed: Bool) {
appearanceDelegate.customPresentationDismissalDidEnd()
}
}
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)
}