I have a tab bar with five items, and I am trying to add a functionality to scroll to the top when the user taps the tab bar item again. Added the UITabBarControllerDelegate to the views where I want to trigger the event and also created a function to determine the selected tab bar index.
When I open the app, index 0 is auto-selected and works perfectly. The view auto scrolls to the top when I scroll down and tap the tab bar index. The problem occurs when I go to index 1 and trigger the scroll there. It somehow completely removes the auto-scroll from my first tab bar item.
Selecting other tab bar items without the auto scroll does not affect index 0 at all.
Home (index 0)
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
self.collectionView?.setContentOffset(CGPoint(x: 0, y: -10), animated: true)
}
}
Users (index 1)
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 1 {
self.tableView?.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
}
Anything with a delegate property can only have one delegate assigned to it at any given time. What ever set the delegate most recently will receive the next delegate method call.
In your case you can probably reset the tab controller's delegate to self in each view controller's viewDidAppear method since you want the currently visible view controller to be the current tab controller delegate.
Add the following to each view controller that needs to be the tab controller's delegate:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.tabBarController?.delegate = self
}
Related
I am presenting a UISearchController programmatically, without adding it to the navigationItem. The Calendar app does something similar.
Without a presentation context, the search bar appears correctly, but persists after pushing another view controller.
This is expected, so we need to set definesPresentationContext on the list view controller... But that causes the search bar to render incorrectly.
Here's the code for context:
private lazy var searchController: UISearchController! = {
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
// If this is set to true, the search bar animates correctly, but that's
// not the effect I'm after. See the next video.
searchController.hidesNavigationBarDuringPresentation = false
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
definesPresentationContext = true
searchButton.rx.tap.subscribe(onNext: { [unowned self] in
present(searchController, animated: true)
}).disposed(by: disposeBag)
}
Setting hidesNavigationBarDuringPresentation kind of fixes it, but we lose the tab bar, and the whole thing just looks bad.
I tried this solution (Unable to present a UISearchController), but it didn't help.
Any suggestions?
UPDATE: The issue is, more specifically, that the search bar appears behind the translucent navigation bar. Making the nav bar solid ( navigationController?.navigationBar.isTranslucent = false) makes the search bar appear under the nav bar.
I have the same problem not been able to solve this either. It seems like the problem is that either
a) the searchcontroller is presented at the very top of the viewcontroller stack, even above the navigation controller, so that it stays active into the next viewcontroller push. or,
b) the searchcontroller is presented underneath the navigationcontroller so that it remains covered by the navigation bar
One idea: don't embed the viewcontroller which is presenting the searchcontroller in a navigation controller. instead, just create a UIView which looks like a navigation bar a the top. would this be an inappropriate solution?
I haven't found a solution to the original problem, but I found a workaround: intercept navigation events, and manually dismiss the search controller.
override func viewDidLoad() {
...
// This makes the search bar appear behind the nav bar
// definesPresentationContext = true
navigationController?.delegate = self
}
extension JobListViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController, animated: Bool) {
// `animated` is false because, for some reason, the dismissal animation doesn't start
// until the transition has completed, when we've already arrived at the new controller
searchController.dismiss(animated: false, completion: nil)
}
}
I found that presenting the search controller over the navigation bar can be achieved by calling present(_:animated:completion:) on the navigation controller itself rather than the navigation controller's child.
So in your view controller you can do
navigationController?.present(searchController, animated: true)
And this will behave like the search button in the Apple's Calendar app.
Update
Regarding dismissing the search controller before pushing a new controller to the navigation stack, you can do this manually depending on how the push is done.
All the bellow will animate dismissing the search controller before the push happens. Note that I disable user interaction until the dismiss animation completes to prevent pushing the same view controller multiple times.
UIStoryboardSegue
Add this override to your view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if navigationController?.presentedViewController == searchController {
view.isUserInteractionEnabled = false
searchController.dismiss(animated: true) {
self.view.isUserInteractionEnabled = true
}
}
}
IBAction (Programmatically)
Just dismiss the search controller before pushing view controllers to the navigation stack:
#IBAction func showSecondTapped(_ sender: UIButton) {
// Dismiss the search controller first.
view.isUserInteractionEnabled = false
searchController.dismiss(animated: true) {
self.view.isUserInteractionEnabled = true
}
// Build and push the detail view controller.
if let secondViewController = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") {
navigationController?.pushViewController(secondViewController, animated: true)
}
}
Handling pop gesture
If the view controller that is presenting the search controller is not the root of your navigation controller, the user might be able to use the interactive pop gesture, which will also keep the search controller presented after the pop. You can handle this by making your view controller the delegate for the search controller, conform to UISearchControllerDelegate and adding the following code:
extension ViewController: UISearchControllerDelegate {
func willPresentSearchController(_ searchController: UISearchController) {
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
func willDismissSearchController(_ searchController: UISearchController) {
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
}
I'm wanting to scroll a table view to the top when someone clicks the tab bar icon.
My tab bar has 4 tabs, the table is on index 0.
I can currently get it to scroll to the top correctly, but I only want it to do it when on index 0, not when returning from the other items.
Currently I have
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
}
What I want is if you are on index 0, then scroll back to the top, if you are going to 0 from another tab, then leave the scroll as it is.
Edit: It dawned on me you can do the whole process on shouldSelect
You need to know which tab was previously selected. One way to do this is to move the logic to "tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)"
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// first condition checks the selected tab is the current view controller
// second verifies you are not coming from another tab
if viewController == self && viewController == tabBarController.selectedViewController {
tableView?.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
return true
}
I have 2 tabs : Calc VC and Browse VC; they are in that order and the app starts on Calc VC.
Using a print statement tabbarcontroller.selectedindex in viewwillappear of each VCs I learned that when Calc VC first appear, it shows an index of 0. When I tap on Browse VC, it shows an index of 1. So far so good.
When I tap on Calc VC, its index becomes 1 and Browse VC becomes 0. It remains like that until you quit the app.
Why it's an issue? I'm trying to disable Browse VC when Calc VC is active by using .isEnabled = false but I can't do it that way because of the changing index
At the time of viewWillAppear triggered, selectedIndex might not be changed. You should use viewDidAppear.
override func viewDidAppear(_ animated: Bool) {
print(tabBarController?.selectedIndex)
}
Or you can use tabBar(_:didSelect:) of UITabBarDelegate
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
print(tabBar.items?.index(of: item))
}
I have a custom TabBarControllerand on one of the Tab Bar Items I have a round red counter. The first Tab Tab Item presents a summary View Controller. The second Tab Bar Item presents a menu embedded in a Navigation Controller The Tab Bar Counterworks fine across the board, except when I segue back from the summary View Controller. Then the counter disappears. I have tried to load the counter in the customTabBarController:
import UIKit
class CustomTabBarController: UITabBarController {
override func viewDidAppear(_ animated: Bool) {
if tabBarCounter != 0
{
let vc = self.tabBarController?.viewControllers?.last
vc?.tabBarItem.badgeValue = "\(tabBarCounter)"
vc?.tabBarItem.badgeColor = UIColor.red
}
}
}
Screendump of storyboard. (The bottom ViewController is the one I´m having trouble with. Segue highlited)
I have 3 tabs in a Tab Bar Controller: Main, TabOne, TabTwo(tab index 0,1,2 respectively)
Main view controller has a table view will show all the elements in a an array. In view controllers TabOne and TabTwo, they all have buttons:
#IBAction func mypost(_ sender: Any) {
tabBarController?.selectedIndex = 0}
The array in Main view controller should be changed differently based on the tab index of the previous view controller. Ex.
If the previous view controller is TabTwo, then the table view will only show all the elements of even indices
BUT
If the previous view controller is TabOne, then the table view will only show all the elements of odd indices
How can I do it?
One possible solution is to have your main view controller set itself up as the delegate of the tab bar controller. Then implement the delegate method shouldSelect and look at the tab controller's currently selected index.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool
let index = tabBarController.selectedIndex
if index == 1 {
// load data appropriate for coming from the 2nd tab
} else index == 2 {
// load data appropriate for coming from the 3rd tab
}
return true
}