Remove/Hide UITabBarItem in Swift - swift

I have looked really hard for this solution in Swift but am not coming up with one that works for me. I am trying to hide my "Admin" TabBarItem based on the permissions of the person that logs in to the app. I can disable it but it still shows up on the bar. I want to be able to show it for certain people and hide it for others. Also, when I print self.tabBarController?.viewControllers I get nil.
class TabBarMenuController: UITabBarController {
let ref = Firebase(url: "")
var position = ""
func getPosition() {
let userRef = ref.childByAppendingPath("users/\(ref.authData.uid)")
userRef.observeSingleEventOfType(.Value, withBlock: {snapshot in
if snapshot.value["position"] as! String != "Staff" {
self.position = snapshot.value["position"] as! String
}
})
}
override func viewWillAppear(animated: Bool) {
getPosition()
print(self.tabBarController?.viewControllers)
if position != "Staff" {
if let tabBarController = self.tabBarController {
let indexToRemove = 3
if indexToRemove < tabBarController.viewControllers?.count {
var viewControllers = tabBarController.viewControllers
viewControllers?.removeAtIndex(indexToRemove)
tabBarController.setViewControllers(viewControllers, animated: true)
}
}
}
}
Also, I keep reading that this is against Apple's intended use. Is that true still? Is there a better workflow to accomplish that type of functionality?

I would create a tab that opens up the user's account and have a button in the user VC tab that opens up a page for admins only. you can show and hide the button as needed using adminButton.hidden = true or adminButton.hidden = false.

Related

The screen comes out from behind

I don't know why this is happening, already I have a similar functions on the project and this isn't happening.
When I delete the account and I to the previous screen or if the user is on background and open it again the 2 methods below cause you to be sent to the main screen but I detected this problem (in the picture)
There is the function when I go to the home when I delete the account
private func goToHomeWithLogin(){
let home = HomeAssembly.presenterView()
Utils.getTopViewController()?.present(home, animated: true)
And the getTopViewController do this:
func getTopViewController() -> UIViewController? {
if let viewController = UIApplication.shared.keyWindow?.rootViewController {
if let modal = UIApplication.shared.keyWindow?.rootViewController?.presentedViewController {
return modal
} else if let navigationController = viewController as? UINavigationController {
return navigationController
}
}
return nil
}
When I drag down the screen comes out from behind
looks like default card style of UIViewController.present(...) if you want it full screen (without the swipe down feature) try home.modalPresentationStyle = .fullScreen before presenting it
let home = HomeAssembly.presenterView()
home.modalPresentationStyle = .fullScreen
Utils.getTopViewController()?.present(home, animated: true)

fatal errors with optionals not making sense

I keep getting a fatal error saying how a value was unwrapped and it was nil and I don't understand how. When I instantiate a view controller with specific variables they all show up, but when I perform a segue to the exact VC, the values don't show up.
Take these functions for example...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let displayVC = storyboard?.instantiateViewController(withIdentifier: Constants.Storyboards.TeachStoryboardID) as? SchoolEventDetailsViewController {
displayVC.selectedEventName = events[indexPath.row].eventName
displayVC.selectedEventDate = documentsDate[indexPath.row].eventDate
displayVC.selectedEventCost = documentsCost[indexPath.row].eventCost
displayVC.selectedEventGrade = documentsGrade[indexPath.row].eventGrade
displayVC.selectedEventDocID = documentsID[indexPath.row]?.docID
navigationController?.pushViewController(displayVC, animated: true)
}
}
This combined with this function :
func verifyInstantiation() {
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = dateToLoad
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = costToLoad
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = gradesToLoad
}
if let docIDtoLoad = selectedEventDocID {
docIDUneditableTextF.text = docIDtoLoad
}
if let eventNameToLoad = selectedEventName {
eventNameEditableTextF.text = eventNameToLoad
}
}
Helps load the data perfectly, but when I try to perform a segue from a search controller the data is not there.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
I set the title of the vc to have the event name , and I also recently added a text field to store it as well for experimental purposes (this question).
Now the issue is I want to do a data transfer from an Algolia Search Controller to that VC and I got all the other fields to show up, except for one and that was the document ID. So I created a completion handler function to get the document ID as a string and have it inserted into the vc when the segue is performed, just like how it's there when the vc is instantiated.
Here is the function :
func getTheEventDocID(completion: #escaping ((String?) -> ())) {
documentListener = db.collection(Constants.Firebase.schoolCollectionName).whereField("event_name", isEqualTo: selectedEventName ?? navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.documentsID = querySnapshot!.documents.map { document in
return EventDocID(docID: (document.documentID) as! String)
}
let fixedID = "\(self.documentsID)"
let substrings = fixedID.dropFirst(22).dropLast(3)
let realString = String(substrings)
completion(realString)
}
}
}
I thought either selectedEventName or navigationItem.title would get the job done and provide the value when I used the function in the data transfer function which I will show now :
//MARK: - Data Transfer From Algolia Search to School Event Details
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
}
}
}
}
But it ends up showing nothing when a search result is clicked which is pretty upsetting, I can't understand why they're both empty values when I declared them in the SchoolEventDetailsVC. I tried to force unwrap selectedEventName and it crashes saying there's a nil value and I can't figure out why. There's actually a lot more to the question but I just tried to keep it short so people will actually attempt to read it and help since nobody ever reads the questions I post, so yeah thanks in advance.
I'm a litte confused what the otherVC is, which sets a property of itself in the getTheEventDocID, whilste in the closure you set the properties of self, which is a different controller. But never mind, I hope you know what you are doing.
Since getTheEventDocID runs asynchronously, the view will be loaded and displayed before the data is available. Therefore, viewDidLoad does not see the actual data, but something that soon will be outdated.
So, you need to inform the details view controller that new data is available, and refresh it's user interface. Something like
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
otherVC.getTheEventDocID { (eventdocid) in
if let id = eventdocid {
if segue.identifier == Constants.Segues.fromSearchToSchoolEventDetails {
let vc = segue.destination as! SchoolEventDetailsViewController
vc.selectedEventName = self.nameTheEvent
vc.selectedEventDate = self.dateTheEvent
vc.selectedEventCost = self.costTheEvent
vc.selectedEventGrade = self.gradeTheEvent
vc.selectedEventDocID = id
vc.updateUI()
}
}
}
}
and in the destination view controller:
class SchoolEventDetailsViewController ... {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
func updateUI () {
navigationItem.title = selectedEventName
// and so on
}
}
Ok so I decided to attempt a workaround and completely ditched the getTheEventDocID() method because it was just causing me stress. So I decided to ditch Firebase generated document IDS and just use 10 digit generated ids from a function I made. I also figured out how to add that exact same 10 digit id in the Algolia record by just storing the random 10 digit id in a variable and using it in both places. So now instead of using a query call to grab a Firebase generated document ID and have my app crash everytime I click a search result, I basically edited the Struct of the Algolia record and just added an eventDocID property that can be used with hits.hitSource(at: indexPath.row).eventDocID.
And now the same way I added the other fields to the vc by segue data transfer, I can now do the same thing with my document ID because everything is matching :).

Update Tab Item Badge after getting data (Swift)

I'd like to update a Badge on a Custom Tab Bar Item when I receive some data. I am able to update the badge on the initial viewDidLoad() but then when I try to call viewDidLoad again later with the data, my tab items are nil. Here is how I have set it up...
class CustomTabBar: UITabBarController {
var count = 0
override func viewDidLoad() {
super.viewDidLoad()
print("this prints correctly every time I call reload with the updated count: " + count)
if let tabItems = self.tabBar.items {
let tabItem = tabItems[0]
tabItem.badgeValue = String(count)
}else{
print("tab items nil")
}
}
func reload(count: Int){
self.count = count
viewDidLoad()
}
}
I'm calling reload() from another view controller after I get the data I need.
func updateBadge(){
let tabBar = CustomTabBar()
let username = UserUtil.username
let db = Firestore.firestore()
let upcomingContestsRef = db
.collection("NBAContests")
.whereField("EnteredUsers", arrayContains: username)
.whereField("Stage", isEqualTo: 2)
upcomingContestsRef.getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
print("count is " + String(querySnapshot!.count))
tabBar.reload(count: querySnapshot!.count)
}
}
}
I've check that viewDidLoad is getting called each time in custom tab bar controller, but after the initial load I don't have access to change the tab items anymore.
Does anyone know whats going on?
I've check out these similar questions
Reload / Refresh tab bar items in a ViewController ?
Setting badge value in UITabBarItem in UIViewController
This creates
let tabBar = CustomTabBar()
a new instance instead you need
guard let tabBar = self.tabBarController as? CustomTabBar else { print("returned") ; return }

How to add `toggleSidebar` NSToolbarItem in Catalyst?

In my app, I added a toggleSidebar item to the NSToolbar.
#if targetEnvironment(macCatalyst)
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.toggleSidebar, NSToolbarItem.Identifier.flexibleSpace, AddRestaurantButtonToolbarIdentifier]
}
}
#endif
However, when I compile my app to Catalyst, the button is disabled. Does anybody know what else I need to do to hook it up?
If you look at the documentation for .toggleSidebar/NSToolbarToggleSidebarItemIdentifier you will see:
The standard toolbar item identifier for a sidebar. It sends toggleSidebar: to firstResponder.
Adding that method to your view controller will enable the button in the toolbar:
Swift:
#objc func toggleSidebar(_ sender: Any) {
}
Objective-C:
- (void)toggleSidebar:(id)sender {
}
Your implementation will need to do whatever you want to do when the user taps the button in the toolbar.
Normally, under a real macOS app using an NSSplitViewController, this method is handled automatically by the split view controller and you don't need to add your own implementation of toggleSidebar:.
The target needs changed to self, this is shown in this Apple sample where it is done for the print item but can easily be changed to the toggle split item as I did after the comment.
/** This is an optional delegate function, called when a new item is about to be added to the toolbar.
This is a good spot to set up initial state information for toolbar items, particularly items
that you don't directly control yourself (like with NSToolbarPrintItemIdentifier).
The notification's object is the toolbar, and the "item" key in the userInfo is the toolbar item
being added.
*/
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .print {
addedItem.toolTip = NSLocalizedString("print string", comment: "")
addedItem.target = self
}
// added code
else if itemIdentifier == .toggleSidebar {
addedItem.target = self
}
}
}
And then add the action to the scene delegate by adding the Swift equivalent of this:
- (IBAction)toggleSidebar:(id)sender{
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
[UIView animateWithDuration:0.2 animations:^{
splitViewController.preferredDisplayMode = (splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModePrimaryHidden ? UISplitViewControllerDisplayModePrimaryHidden : UISplitViewControllerDisplayModeAllVisible);
}];
}
When configuring your UISplitViewController, set the primaryBackgroundStyle to .sidebar
let splitVC: UISplitViewController = //your application's split view controller
splitVC.primaryBackgroundStyle = .sidebar
This will enable your NSToolbarItem with the system identifier .toggleSidebar and it will work automatically with the UISplitViewController in Mac Catalyst without setting any target / action code.
This answer is mainly converting #malhal's answer to the latest Swift version
You will need to return [.toggleSidebar] in toolbarDefaultItemIdentifiers.
In toolbarWillAddItem you will write the following (just like the previous answer suggested):
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .toggleSidebar {
addedItem.target = self
addedItem.action = #selector(toggleSidebar)
}
}
}
Finally, you will add your toggleSidebar method.
#objc func toggleSidebar() {
let splitController = self.window?.rootViewController as? MainSplitController
UIView.animate(withDuration: 0.2) {
splitController?.preferredDisplayMode = (splitController?.preferredDisplayMode != .primaryHidden ? .primaryHidden : .allVisible)
}
}
A few resources that might help:
Integrating a Toolbar and Touch Bar into Your App
Mac Catalyst: Adding a Toolbar
The easiest way to use the toggleSidebar toolbar item is to set primaryBackgroundStyle to .sidebar, as answered by #Craig Scrogie.
That has the side effect of enabling the toolbar item and hiding/showing the sidebar.
If you don't want to use the .sidebar background style, you have to implement toggling the sidebar and validating the toolbar item in methods on a class in your responder chain. I put these in a subclass of UISplitViewController.
#objc func toggleSidebar(_ sender: Any?) {
UIView.animate(withDuration: 0.2, animations: {
self.preferredDisplayMode =
(self.displayMode == .secondaryOnly) ?
.oneBesideSecondary : .secondaryOnly
})
}
#objc func validateToolbarItem(_ item: NSToolbarItem)
-> Bool {
if item.action == #selector(toggleSidebar) {
return true
}
return false
}

How call delegate function without present controller

I have Tap Bar Controller with 2 paths. One is settingController Other one is loginController and contactListController.
When i run the program the entry pint is set tocontactListControllerand if login is false apps shownloginController`. After login value is set on true and loginController is dismiss. On bottom i have Tab Bar Controller: ContactList | Settings
When i go to settings i have a LOGOUT button, i would like to do when i tap this how to set value login on false ? i have no segue between ContactList and SettingController
This is my ContactListController
class ContactsTableViewController: UITableViewController, SettingsControllerDelegate {
let settingsController: SettingsController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("settingsController") as! SettingsController
override func viewDidLoad() {
super.viewDidLoad()
settingsController.delegate = self
}
func didLogoutSuccessfully() {
loggedIn = false
}
}
This is settings controller
protocol SettingsControllerDelegate {
func didLogoutSuccessfully()
}
class SettingsController: UITableViewController {
var delegate: SettingsControllerDelegate?
fun tapButton() {
self.delegate?.didLogoutSuccessfully() // Set login as false
}
if i added
override func viewDidLoad() {
super.viewDidLoad(
settingsController.delegate = self
presentViewController(settingsController, animated: true, completion: nil)
}
my controller setting is appear first. How can i change this value in other way?
UPDATE
in contact list i have
var loggedIn: Bool = false {
didSet {
if loggedIn == true {
self.configureView()
}
}
}
override func viewDidAppear(animated: Bool) {
if loggedIn == false {
performSegueWithIdentifier("showLogin", sender: nil)
}
//tableView.reloadData()
}
Consider either 1: using notifications (to which all interested controllers are registered as observers) to react to session state changes, 2: moving your session state to something "higher up the chain" (like in or "hanging off of" your app delegate), or 3: making a singleton session controller.
1 can be used with 2 and 3 and either 2 or 3 make accessing the current state from anywhere in your app easier. I'd go with a mix of 1 and 3 myself.
This approach in general relieves you from having to walk and inspect the controller hierarchy to find and set the same thig on all other controllers (which is icky because it's so tightly coupled; changing the hierarchy and/or reusing VCs elsewhere would probably break things).
You can pass data between the tabs using UITabViewControllers.viewControllers method which returns array of the view controllers in the tab
//in SettingsVC
func viewWillDisapear(){
//assuming its in the second index of tabBar
let contactVC = self.tabBarController.viewControllers[1] as ContactsTableViewController
contactVC.delegate = self
contactVC.loggedIn = true //or false as you wish
super.viewWillDisapear()
}