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}
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'm using BulletinBoard (BLTNBoard) to create dialogs in my iOS app. There's an option to embed image inside it. I would like to extend it's functionality and allow user to manipulate this image using tap gesture. But eventually when I assign a gesture to it's imageView using addGestureRecognizer nothing happens.
Here's how I initiliaze bulletin and add gesture to the image:
class ViewController: UIViewController {
lazy var bulletinManager: BLTNItemManager = {
let rootItem: BLTNPageItem = BLTNPageItem(title: "")
return BLTNItemManager(rootItem: rootItem)
}()
override func viewDidLoad() {
//etc code
let bulletinManager: BLTNItemManager = {
let item = BLTNPageItem(title: "Welcome")
item.descriptionText = "Pleas welcome to my app"
item.actionButtonTitle = "Go"
item.alternativeButtonTitle = "Try to tap here"
item.requiresCloseButton = false
item.isDismissable = false
item.actionHandler = { item in
self.bulletinManager.dismissBulletin()
}
item.alternativeHandler = { item in
//do nothing by now
}
//
item.image = UIImage(named: "welcome")
//adding gesture to its imageView
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
return BLTNItemManager(rootItem: item)
}()
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
and nothing happens at all (no message printed in console).
However if I assign action inside alternative button it works as expected:
item.alternativeHandler = { item in
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
}
I guess the only thing which can prevent me to assign the tap event to it properly is that imageView becomes available much later than the bulletin is created (for example only when it is shown on the screen).
Could you please help and correct my code. Thanks
upd.
Ok, based on Philipp's answer I have the following solution:
class myPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
let imageView=super.imageView
imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: #selector(tapTap))
imageView?.addGestureRecognizer(tap)
return contentViews
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
When you're working with an open source library, it's easy to check out the source code to find the answer.
As you can see here, image setter doesn't initiate the image view.
Both makeContentViews makeArrangedSubviews (which are responsible for views initializing) doesn't have any finish notification callbacks.
Usually in such cases I had to fork the repo and add functionality by myself - then I'll make a pull request if I think this functionality may be needed by someone else.
But luckily for you the BLTNPageItem is marked open, so you can just subclass it. Override makeContentViews and add your logic there, something like this:
class YourOwnPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
// configure the imageView here
return contentViews
}
}
I got tab bar controller with three view controllers setup. One of those view controllers changes its tab bar item badgeValue when I open it. I would like to change this badgeValue already when I arrive at the first tab.
I created a Tabbarcontoller: UITabBarController class but don't know how to easily access the items of the sub views. Here is the code from my Tabbarcontoller class:
class TabBarController: UITabBarController, MainMethodsDelegate {
var myFriendsRequests: [UserInfo] = []
var friendRequestsCount: Int = 0
func getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) {
myFriendsRequests.append(myFriendsRequest)
self.friendRequestsCount = myFriendsRequests.count
DispatchQueue.main.async {
//friendsBarItem.badgeValue = String(self.friendRequestsCount)
}
}
let mainMethods = MainMethods()
override func viewDidLoad() {
super.viewDidLoad()
mainMethods.delegate = self
mainMethods.getFriendsRequests()
}
}
And here the working code from the sub view controller:
class FriendsViewController: UIViewController, MainMethodsDelegate {
var myFriendsRequests: [UserInfo] = []
var friendRequestsCount: Int = 0
func getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) {
myFriendsRequests.append(myFriendsRequest)
self.friendRequestsCount = myFriendsRequests.count
DispatchQueue.main.async {
self.friendsBarItem.badgeValue = String(self.friendRequestsCount)
}
}
let mainMethods = MainMethods()
override func viewDidLoad() {
super.viewDidLoad()
mainMethods.delegate = self
mainMethods.getFriendsRequests()
}
#IBOutlet weak var friendsBarItem: UITabBarItem!
}
I'm looking for a simple way to access the sub view controllers or at least the bar items where it says:
//friendsBarItem.badgeValue = String(self.friendRequestsCount)
I'm not sure if delegates is the right way to go?
I would leave the bar item out of the friends controller. It does not need to know about the tabBar. If I were you, I would create a protocol to let know your tabBarController that the number of friend requests has changed.
First, define the protocol:
protocol FriendsRequestDelegate {
func friendsRequestsDidChange(number: Int)
}
Then, add the variable to your FriendsViewController:
weak var delegate: FriendsRequestDelegate?
And, we need to trigger that func, still in FriendsViewController, after fetching the number of friends request, add;
delegate?.friendsRequestsDidChange(number: myFriendsRequests.count)
Finally, make your TabBarController conform to this protocol;
extension TabBarController: FriendsRequestDelegate {
func friendsRequestsDidChange(number: Int) {
friendsBarItem.badgeValue = String(number)
}
}
See what I mean? This way your friendsRequestController doesn't know about the tabBar and it'll keep your code clean.
I've found a quite simple way to access the barItems. I only had to access the array of tabBar items with:
tabBar.items![2].badgeValue = ""
Here is my code for the UITabBarController:
class TabBarController: UITabBarController, MainMethodsDelegate {
var myFriendsRequests: [UserInfo] = []
var friendRequestsCount: Int = 0
func getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) {
myFriendsRequests.append(myFriendsRequest)
self.friendRequestsCount = myFriendsRequests.count
DispatchQueue.main.async {
self.tabBar.items![2].badgeValue = String(self.friendRequestsCount)
}
}
let mainMethods = MainMethods()
override func viewDidLoad() {
super.viewDidLoad()
mainMethods.delegate = self
mainMethods.getFriendsRequests()
}
}
The sub view controller does not need to check and update the badgeValue anymore. (This is fine in my case since the getFriendsRequests_Methods_Destination(myFriendsRequest: UserInfo) method gets triggered as soon as there is a new friends request anyways using a (Firestore) snapshot listener.)
My initial way was to access the tabItem from each of the sub view controllers individually using tabBarController!.tabBar.items![2].badgeValue = "" But this would have been redundant in my case.
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 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...