UI testing a tab bar controller - ios

I have built a simple tab bar with 3 tabs.
I want to run a UI test to make sure that if the user clicks a tab bar item, the correct view controller shows. How would I go about doing that? Below is the code I would start with, just don't know how to write my assertion.
func testTabBarMyProfileButton() {
let tabBarsQuery = XCUIApplication().tabBars
tabBarsQuery.buttons["My Profile"].tap()
}
func testTabBarGraphsButton() {
let tabBarsQuery = XCUIApplication().tabBars
tabBarsQuery.buttons["Graphs"].tap()
}
func testTabBarAboutButton() {
let tabBarsQuery = XCUIApplication().tabBars
tabBarsQuery.buttons["About"].tap()
}

You can access the tabbar button by its position:
app.tabBars.buttons.element(boundBy: 2).tap()

If you have different controls in each view controller shown on each tab bar, you can make assertions if they exist or not (what is expected).
For example if the first tab bar has UILabel named "First name" you can assert if it exists by writing
Let theLabel = app.staticTexts["myValue"]
XCTAssert(theLabel.exists).to(beTrue)
And on the other screens do the same thing for the different controls.

If anyone finds this looking to UI test the contents of another app, I just found a solution..
The tab bar item is a lazy variable and needs to be touched before you can reference a tab bar button by value. Add this line:
tabBarItem.accessibilityIdentifier = "my-snazzy-identifier"
to the viewDidLoad method and you should be able to do this in your UI tests:
app.tabBars.buttons["Button Title"].tap()

You can test the title of the navigation bar.
XCTAssert(app.navigationBars["Graphs"].exists)
See my GitHub repo for a more detailed UI Testing example.

Related

How can I disable some of the tabs of the moreNavigationController of my UITabBarController?

I'm using XCode 13.0, Swift 5.5 and Storyboard. This is a mobile app for iPhone with iOS 15.
I have altogether 7 tabs, all of them with icons. I have 4 tabs and a "More" tab in the tab bar of my app.
3 additional tabs show up after the user clicks on the "More" tab. I'd like the first 2 of these additional tabs to be disabled. They need to be grey coloured and when the user clicks on any of these 2, I'd like to app to do nothing (not to show any page, not to navigate anywhere). I'm using the original UITabBarController and moreNavigationController defined by Apple, I didn't subclass any of them.
On my storyboard I added 2 UIViewControllers to these 2 disabled tabs, but I set them disabled this way:
This didn't work at all. When the user clicks on the 2 disabled tabs, they show 2 empty ViewControllers. I'd expect them not to show anything.
I also tried accessing the moreNavigationController's tabs from the page my UITabBarController first opens. I tried setting these 2 tabs disabled programmatically. However I didn't manage to access these 2 tabs, I only manage to access the main tabs that show up in the tab bar (I don't need to access these). How can I disable and colour grey the tabs that show up after clicking on the More tab?
let moreControllerItems = tabBarController?.moreNavigationController.toolbarItems
if let tabArray = moreControllerItems {
let tabBarItem1 = tabArray[0]
let tabBarItem2 = tabArray[1]
tabBarItem1.isEnabled = false
tabBarItem2.isEnabled = false
}`
This code doesn't work, the moreControllerItems variable is nil. The tabBarController variable isn't nil, I can access my UITabBarController from here, but it doesn't help me much.
I googled this issue lots of different ways but I couldn't find the solution. Any help would be appreciated. I really need to solve this, please write a comment if you have any idea how to solve this. Thank you!
You can disable tabBar items from TabBarController.
class TabVC: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
func disableTabBar(itemNo: Int) {
if let items = tabBar.items, itemNo < items.count {
items[itemNo].isEnabled = false
}
}
And you can access this function from
any child Viewcontroller attached to TabBarController
class MoreVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let tabBarVC = tabBarController as? TabVC {
tabBarVC.disableTabBar(itemNo: 1)
}
}
In short: You cannot disable it. If you want to disable the item in the More Viewcontroller, you have to use a custom Viewcontroller in the fifth Tab
I am explaining here why we cannot do it and what limitations we have.
Apple will show the More tab if items are more than five in the UITabBarController. And more tab holds the UINavigationController which is attached to a kind of UIMoreListController. you can check by
print moreNavigationController.viewControllers[0]
UIMoreListController is not accessible. In your case, the remaining three items are listed in the UIMoreListController. Although we can still access the Viewcontroller's tabBarItem and we can disable it. You can check by clicking the edit option.
But still, users can click on it as long as it is in the UIMoreListController. Because we are not interacting with the tabBar instead we are interacting with the items on the list. Users can't click once you move the item to any of the first four positions using the edit option in the MoreViewcontroller.

How to hide PTCardTabBar?

I use PTCardTabBarController like custom tab bar. I want to hide tab bar in my ViewController. I trying to use in storyboard Hide Bottom Bar on Push or in code: self.tabBarController?.tabBar.isHidden = true. But it not helped me.
When I stat to use PTCardTabBar I have next scheme in storyboard:
TabBarController (with class PTCardTabBarController and module PTCardTabBar) -> NavigationBarController -> ViewController.
Next I launch my app and I see under my PTCardTabBar system iOS tab bar. I use in storyboard Hide Bottom Bar on Push but it is hide only system tab bar and not PTCardTabBar. How to fix it and hide both tab bars?
Taking a quick look at that PTCardTabBar library...
What you see on-screen is not a UITabBar ... it is a UIView. So, any normal actions on a standard Tab Bar Controller's TabBar will not be related.
To hide it, you need to edit PTCardTabBarController.swift and make its customTabBar available to your code:
// make this "open"
open lazy var customTabBar: PTCardTabBar = {
return PTCardTabBar()
}()
Then, when you want to hide the tab bar (for example, in viewDidLoad() in your view controller:
if let ptcTBC = tabBarController as? PTCardTabBarController {
ptcTBC.customTabBar.isHidden = true
}
You'll also (obviously) need to set .isHidden back to false to show it again.
Probably, though, you want to do more than just have it showing or hidden... in which case you could further modify PTCardTabBarController.swift to add a function to animate it into or out-of view (for example).

Swift hide navigation title but show its title as back button in next view controller

I am using tab bar controller as main and following way-
UITabbarController -> UINavigationController -> UITableViewController (with 5 tab bar & uncheck show navigation bar & made UIView with mail box image as button then click ) -> ViewController (with 5 tab bar & without back button )
I want to get back button as first tab bar title name like < Home
Please let me know the programmatic or structure way.
Thanks
Try to hide titleView label
self.navigationItem.titleView = UIView()
You need to remove the text from the back button. The simplest way is to set it as empty string, like this
self.navigationController?.navigationBar.backItem?.title = ""
You are looking for this
self.navigationController?.navigationBar.backItem?.title = "TEXT"

How to add a tabbar after a user action

I have an app that doesn't have to show any tab when the user isn't logged in. I'm facing many issues with this functionality. First of all, I had embed the views, after the login screen, into a tab bar controller and everything was showing ok, except when I had implemented the login feature. As it's an async call, I had to wait until the credentials were validated. I wasn't able to do this in the shouldPerformSegue method or any other method provided by apple because you can't block the main thread until the async stuff is done, so the segue has to be done programaticaly in an IbAction:
#IBAction func doLogin(sender: AnyObject) {
userIsLogged = false
let apiCall = webApi()
apiCall.callCheckIsUserLogged(nil, password : self.passwordField.text, email: self.mailField.text){ (ok) in
if ok {
if(userIsLogged == true){
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("loginUser", sender: self)
}
}else {
NSOperationQueue.mainQueue().addOperationWithBlock{
print("User not logged in")
self.alert.message = "Please enter valid credentials"
self.displayAlert()
}
}
}
}
}
But this has driven me to another issue: after the programatic segue, my tab bar controller was disappearing, and after read a while it looks like the only way to avoid this is to embed your tab bar controller into a navigation controller. So I did it, but now, I got many new issues. First of all I got two navigation controllers, the one who is at the very beginning of the project and this new one I have embed into the tab bar controller. A picture will illustrate this better than my words:
Now I have two navigation controllers, and I don't know how to hide the top one. Already tried:
self.navigationController?.navigationItem.hidesBackButton = true
But is hiding the arrow and I need to hide the other one navigation controller. But the best thing indeed would be to see the best approach for this kind of cases, when you want to add a tabBar controller embed into a navigation controller in the middle of the project.
Thanks all
I guess you can take another approach. Make login storyline and your app storyline distinct.
Have a storyboard for your login procedure, and another storyboard for your home (or whatever you like) and manage them in AppDelegate.
This is how i did it:
if /* user must log in */ {
self.window?.rootViewController = loginStoryboard?.instantiateInitialViewController()
self.window?.makeKeyAndVisible()
}
else {
self.window?.rootViewController = homeStoryboard?instantiateInitialViewController()
self.window?.makeKeyAndVisible()
}
Put this code in a method (for example called manageRootViewController()) and call it at app launch, or after your login. (You can also add custom animations if you like)

Split view in container view loses function of navigation controller

i'm building an app for which i need a split view controller. I embedded it in a container view so i could use a segue to acces it. The problem now is that I can't use my navigation bar in a proper way anymore. Normally, there should be a button to go to the previous page, but now I only have the option to go to the homepage (for within the split view controller). You can find some images here. Also, the title will always be "Shoppen", even though I stated it otherwise.
I suppose the problem is that the navigation bar of the view controller in which the container view is set will always be the top one. I have no idea on how to either delete or fix this problem though, could anybody here help me with this? Thanks in advance!
Some code to show you what it should normally do:
In the first section, the table view, it should indeed always title "Shoppen" and able to go back to the homepage
In the detail section, we should be able to go to the table view page, and the title should be the category of the shop we clicked. This is defined here:
func configureView() {
if let detailWinkel = detailWinkel {
if let detailDescriptionLabel = detailDescriptionLabel, WinkelImageView = WinkelImageView {
if detailWinkel.name == "Hunkemoller" {
detailDescriptionLabel.text = "Hunkemöller"
} else {
detailDescriptionLabel.text = detailWinkel.name
}
WinkelImageView.image = UIImage(named: detailWinkel.name)
title = detailWinkel.category
StraatDescriptionLabel.text = detailWinkel.straat
AdresDescriptionLabel.text = detailWinkel.adres
WebsiteButton.setTitle("Open website in Safari", forState: UIControlState.Normal)
}
}
}
I don't completely understand what you are trying to do.
An embed segue is not a normal segue. You can't push a view that is embedded into another view controller onto a navigation stack, if that's what you are saying.
An embed segue is a degenerate case. It invokes the contained view controller at the time the parent view controller is loaded, and there is no mechanism for going back.

Resources