I have an item in my tab bar that shouldn't be enabled until certain conditions are met. I can disable that item in viewDidLoad() from my subclassed UITabBarController, but I'm having trouble creating a function that I can call when needed. Below is what I have so far - for reasons I don't understand, my tab bar item array is always nil! (Unless its initialized in viewDidLoad() where it works fine.)
func setTabState(whichTab: Int) {
let arrayOfTabBarItems = self.tabBar.items
if let barItems = arrayOfTabBarItems {
if barItems.count > 0 {
let tabBarItem = barItems[whichTab]
tabBarItem.isEnabled = !tabBarItem.isEnabled
}
}
}
Please put below code where you want to disable tabbar item in your UITabbarController class
//Here Disable 0 Tabbar item
DispatchQueue.main.async {
let items = self.tabBar.items!
if items.count > 0 {
items[0].isEnabled = false
}
}
The solution turned out to be a combination of Rohit Makwana's answer and some experimentation:
In viewDidLoad() of my CustomTabBarViewController I used Rohit's
answer to set the initial state of the tab bar items. I still don't understand why using DispatchQueue is necessary, but one thing at a time.
In a separate view controller I adopted the UITabBarControllerDelegate protocol and set
tabBar?.delegate = self.
Finally, I created a property observer on a variable that gets set to true when certain conditions are met:
var allButtonsPressed = false {
didSet {
if let items = tabBarController?.tabBar.items {
items[1].isEnabled = allButtonsPressed
}
}
}
And it works! When allButtonsPressed is true, the tab bar item is instantly enabled. When it's false - disabled. Plus one to Rohit for helping me get to the solution. Now, off to learn more about DispatchQueue...
Related
We have the following app, where user can switch to different "page" (purple, yellow, ... colours) from side menu.
I was wondering, should the "page" be implemented as UIView, or should the "page" be implemented as UIViewController?
The pages shall responsible to
Read/ write from/ to CoreData.
Possible holding a UIPageView, which user can swipe through multiple child pages as shown in https://i.stack.imgur.com/v0oNo.gif
Holding a UICollectionView.
User can drag and move the items in the UICollectionView
User can perform various contextual action (Delete, clone, ...) on the items in UICollectionView.
Can easily port to iPad in the future.
Currently, my implementation of using UIView are as follow.
private func archive() {
if let trashView = self.trashView {
trashView.removeFromSuperview()
self.trashView = nil
}
if self.archiveView != nil {
return
}
let archiveView = ArchiveView.instanceFromNib()
self.view.addSubview(archiveView)
archiveView.translatesAutoresizingMaskIntoConstraints = false
archiveView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
archiveView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
archiveView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
archiveView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.archiveView = archiveView
}
private func trash() {
if let archiveView = self.archiveView {
archiveView.removeFromSuperview()
self.archiveView = nil
}
if self.trashView != nil {
return
}
let trashView = TrashView.instanceFromNib()
self.view.addSubview(trashView)
trashView.translatesAutoresizingMaskIntoConstraints = false
trashView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
trashView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
trashView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
trashView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.trashView = trashView
}
I notice that if I implement the "pages" using UIView, I will lost some capability of UIViewController like
viewDidLoad callback.
viewWillLoad callback.
viewDidLayoutSubviews callback.
...
However, I am not clear whether losing those capabilities will stop me from implementing a proper "page"?
May I know, should I implement those "pages" using UIView, or using UIViewController?
I would do this with UIViewController because of all the UIKit callback reasons you listed in the question already.
I assume that you have a UINavigationController instance that's set as window.rootViewController for your app. You have a reference to this instance using which you can easily switch between different screens.
Example
class SlideMenuViewController: UIViewController {
enum Option {
case archive
case trash
}
var onSelect: ((_ option: Option) -> Void)?
}
class ArchiveViewController: UIViewController {}
class TrashViewController: UIViewController {}
class AppNavigator {
let mainNavigationController: UINavigationController
init(navigationController: UINavigationController) {
self.mainNavigationController = navigationController
}
private lazy var slideMenuVC: SlideMenuViewController = {
let slideMenu = SlideMenuViewController()
slideMenu.onSelect = { [weak self] (option) in
self?.openScreen(for: option)
}
return slideMenu
}()
private lazy var archiveVC: ArchiveViewController = {
return ArchiveViewController()
}()
private lazy var trashVC: TrashViewController = {
return TrashViewController()
}()
func openScreen(for option: SlideMenuViewController.Option) {
let targetVC: UIViewController
switch option {
case .archive: targetVC = archiveVC
case .trash: targetVC = trashVC
}
mainNavigationController.setViewControllers([targetVC], animated: true)
}
}
Perhaps I totally misunderstood your question, but your UIView should only hold logic related to how the view itself will appear to the user. The UIView should not contain any logic related to the other views or to the model.
UIKit assumes that you use the MVC model to implement apps on the Apple platforms. This means that any code related to controlling which view has to appear and which data the view should get from the model, should be written in the ViewController.
In Xcode you have both the UICollectionViewController and the UIPageViewController to implement page swipes and dragging an dropping views.
View controllers can give the control to other view controllers to present views. The view controller also determine the data that should be presented by the view. Please, check out this article about the MVC model.
Kind regards,
MacUserT
I have UISearchController in the navigationItem.searchController and I want to make it focus when the user selects "Search" from the menu.
So shortly, when the user is tapping on the "Search" option in the menu (UITableViewCell) it's getting the view controller that have the searchController in it and calling:
guard let navigationVC = presentingViewController as? UINavigationController else { return }
guard let documentsVC = navigationVC.topViewController as? DocumentsViewController else { return }
documentsVC.searchController.searchBar.becomeFirstResponder()
Then, the UISearchBar is getting focus, the keyboard is appearing and then it's immediately disappearing, and I don't have any code that would make it disappear (like view.endEditing()).
1 GIF is worth more than 1,000 words:
So, after many tries I got some way to make it work, but I'm sure there is a much more elegant ways to do this, so if someone think that they have better way, please post it here and I may use it and mark your answer as the correct one.
Create the function focusOnSearchBar() in YourViewController:
func focusOnSearchBar() {
let searchBar = searchController.searchBar
if searchBar.canBecomeFirstResponder {
DispatchQueue.main.async {
searchBar.becomeFirstResponder()
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.focusOnSearchBar()
}
}
}
What it actually do is use itself recursively and check (every 0.1 sec) if searchBar.canBecomeFirstResponder. This is the problematic/not elegant thing.
Then, add this to viewDidAppear():
if focusOnSearch {
searchController.isActive = true
}
Don't forget to add extension to your ViewController for UISearchControllerDelegate (and of course, set searchController.delegate = self) and implement didPresentSearchController (that will be invoke by setting searchController.isActive = true):
extension YourViewController: UISearchControllerDelegate {
func didPresentSearchController(_ searchController: UISearchController) {
if focusOnSearch {
focusOnSearchBar()
}
}
}
Now all you have to do is to set focusOnSearch = true in the prepare(for segue:sender:).
*Note: if you want to focusOnSearchBar while you are in the same viewController of the searchBar, just set:
focusOnSearch = true
searchController.isActive = true
And it will work by itself.
Make your searchbar first responder in the viewDidLoad method. That will make sure everything is ready before focusing the search bar.
I have an application that has two UIViewcontroller embedded in a UITabBarcontroller. When I am in UIViewController-1, i would like to press a button that disables all item selection of the tab bar. My effort is below but I am not sure how to complete the code ...
When I am in the 'Folders' UIViewController I would like to disable the selection of any tab bar item:
class Folders: UIViewController, UITableViewDataSource, UITableViewDelegate{
...
// DISABLE TAB BAR ITEMS
func disable (){
let tabBarItemsArray = self.tabBarController?.tabBar.items
tabBarItemsArray[0].enabled = false // THIS BIT OF CODE IS NOT RECOGNIZED BY XCODE
}
...
}
tabBarItemsArray is optional, its type is [UITabBarItem]?.
You could initially force unwrap it: tabBarItemsArray![0], but the right way is to use if let construct:
if let tabBarItemsArray = tabBarController.tabBar.items {
tabBarItemsArray[0].isEnabled = false
}
or:
guard let tabBarItemsArray = tabBarController.tabBar.items else {
fatalError("Error")
}
let item = tabBarItemsArray[0]
item.isEnabled = false
You can do that using single line of code. Please check following code.
You can execute this from any controller.
self.navigationController?.tabBarController?.tabBar.items![0].isEnabled = false
Another way
You can define NotificationCenter observer to achieve. Please check following code. *In TabBar Controller file.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(disableTab(notification:)), name: Notification.Name("disableTab"), object: nil)
}
#objc func disableTab(notification: Notification) {
self.TabBarItem.isEnabled = false
}
Fire from anywhere as following...
NotificationCenter.default.post(name: Notification.Name("disableTab"), object: nil)
if you want to disable one tabbar item at once then this is for disabling the first one:
guard let tabbars = self.tabBar.items else {
return
}
tabbars[0].isEnabled = false
but if you want them all to be disabled at once then this is the one to be implemented:
self.tabBar.items?.map{$0.isEnabled = false}
I'm trying to do a design a XLPagerTabStrip control where the overall theme changes as the tabs change.
The following is what's getting called when a tab changes
changeCurrentIndexProgressive = { [weak self] (oldCell: ButtonBarViewCell?, newCell: ButtonBarViewCell?, progressPercentage: CGFloat, changeCurrentIndex: Bool, animated: Bool) -> Void in
guard changeCurrentIndex == true else { return }
oldCell?.label.textColor = .black
newCell?.label.textColor = UIColor.red
//Change the navigation bar's color
self?.navigationController?.navigationBar.barTintColor = UIColor.red
//Attempting to change the selected bar color
self?.settings.style.selectedBarBackgroundColor = UIColor.green
}
Changing the navigation controllers color works, but I can't get the the selected bar (or any thing under the settings object) to change from within here?
Is it possible to change the settings once the view has been loaded?
Code Check
if you check the code Code Search for selectedBarBackgroundColor then you find this 3 interesting search results:
BaseButtonBarPagerTabStripViewController#viewDidLoad
buttonBarView.selectedBar.backgroundColor = settings.style.selectedBarBackgroundColor
BarPagerTabStripViewController#viewDidLoad
barView.selectedBar.backgroundColor = settings.style.selectedBarBackgroundColor ?? barView.selectedBar.backgroundColor
ButtonBarPagerTabStripViewController#viewDidLoad
buttonBarView.selectedBar.backgroundColor = settings.style.selectedBarBackgroundColor
that means that you need to set the BackgroundColor before an viewDidLoad.
Issue
see also the answer of this issue: XLPagerTabStrip Issue #137:
Actually this isnot a issue. Settings should be configured before viewDidLoad is called. Could you please document it in the readme?
workaround
buttonBarView is a public var maybe you can set it additional to the settings direct this property: buttonBarView.selectedBar.backgroundColor
I currently try to find a way to remove while run the app a TabBar Item, i found a way to enable or disable it but not to complete remove it.
For disable it i do:
In ViewDidLoad
if let tabBarItem = self.tabBarController?.tabBar.items?[3] as? UITabBarItem {
tabBarItem.enabled = false
}
This works well but still the user can see the TabBar item and i ll simply complete remove it, is there a way?
I want to trigger the TabBarItem via Parse, if i set the Parse Data to true it should show other way it should not.
You want to set the viewControllers property of your tabBarController with an array where you excluded the particular viewController that you don't want to have anymore.
if let tabBarController = self.tabBarController {
let indexToRemove = 3
if indexToRemove < tabBarController.viewControllers?.count {
var viewControllers = tabBarController.viewControllers
viewControllers?.remove(at: indexToRemove)
tabBarController.viewControllers = viewControllers
}
}
For those who just want to disable one item. Use this code from #Daniele's solution. and place it in your UITabBarController class
viewDidLoad() {
let index = 0 //0 to 5
viewControllers?.remove(at: index)
}
Swift 5: For removing only one index in Tab Bar Controller(you can use this method in viewDidLoad and viewDidAppear both of them)
override func viewDidAppear(_ animated: Bool) {
}
override func viewDidLoad() {
super.viewDidLoad()
}
tabBarController.viewControllers?.remove(at:0) // for 0 index
tabBarController.viewControllers?.remove(at:1) // for 1 index
tabBarController.viewControllers?.remove(at:2) // for 2 index
if you have 4 index in Tab Bar and you want to remove the last 2 index
tabBarController.viewControllers?.remove(at:2)
tabBarController.viewControllers?.remove(at:2)
first line will remove the index 3rd one and you will remaining 3 from 4 and again when you remove the 2nd index it will remove again 3rd index and then you will have remain 2 index in last.
Another Way
//MARK: - Function Call
removeTab(at: 4)
//MARK: - Method
func removeTab(at index: Int) {
if self.viewControllers?.count ?? 0 >= index {
self.viewControllers?.remove(at: index)
}
}
Swift 4.1
For removing More items Use Array
let index = [2,0]
index.forEach{viewControllers?.remove(at: $0)}
the point in the array is You Should Use Descending Order of indexes to remove to get the desired Result.
better way is to use only text instead of image. choose 'space' as the text then disable that. then the user will not be able to see it. i haven't tested it but I sure it will work.