I am working on making a tabbed app. It has a TabBarController and 4 ViewControllers attached to it.
By default, at launch only FirstViewController is loaded. I would like to load both FirstViewController and SecondViewController at start before switching to the second view via tab menu.
What i tried so far is that I created custom MyTabBarController class and tried to use
var sv = SecondViewController()
sv.loadView()
in ViewDidLoad(), but it caused a fatal error during loading, because (my guess) mapView element from storyboard was not loaded.
What is the correct way to simultaneously load the two viewControllers which use storyboard elements? All my other tries have not been successful so far.
Add in your main view controller
var secondViewController:UIViewController!
And in your viewDidLoad:
secondViewController: UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "yourIdentifier") as! SecondViewController
That's it. When you want to present it, use:
self.present(secondViewController, animated: true, completion: nil)
Related
the thing is im new to ios development this is my second application while working on the first one i mainly used a theme and then worked over it but this one im working from scratch.
My main problem is going from one screen to another
When i use this code it works
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "servicesScreen") as! ServiceViewController
nextViewController.modalPresentationStyle = .fullScreen
self.present(nextViewController, animated:true, completion:nil)
But i need to use this one so i can go back too but this wont do anything
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "servicesScreen") as! ServiceViewController
self.navigationController?.pushViewController(newViewController, animated: true)
My question is, am i doing anything wrong here or do i need to add something in scenedeligate?
try
select Main.storyboard
select initial view Controller
select editor -> Embed in -> navigation controller.
For this, you need to add navigation controller to initial storyboards.
If you are using navigationController you need to add a navigation controller first on storyboard or programmatically.
Programmatically add navigation controller check this link:-
Creating a navigationController programmatically (Swift)
or
To add navigation controller on storyboard you should follow this link:-
https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/ImplementNavigation.html
If you have added navigation controller to your app then make sure your intialViewController is set to navigation controller or your navigation controller is added to the stack when you run your application.
Also check for "Is the identifier correct? Did you reference the correct storyboard?".
I have created a TabBarController with 4 UIViewControllers. However, when I want to show one of the ViewControllers in the TabBarController with:
func gotoMainAppVC()
{
let mainVC:SearchVC = SearchVC()
self.present(mainVC, animated: true, completion: nil)
}
I only get a plain black screen. What am I doing wrong?
This is not how any of this works.
I am not sure what self is in your context but I expect it is either a tab bar controller or one of the view controllers on the tab bar.
By calling present it means you want to present a view controller on a current one which by default brings a new fullscreen view controller from bottom upwards.
Since you have created a new instance of view controller by calling SearchVC() this is not the view controller that you see in your storyboard. You may create any instances of any of your view controllers but in your case if you designed SearchVC in a storyboard you want to actually use that version of it. To do so you need to set storyboard id inside your storyboard. After you have done that you should initialise it as using:
UIStoryboard(name: <#T##String#>, bundle: nil).instantiateViewController(withIdentifier: <#T##String#>)
as in
let mainVC:SearchVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "TheIDIHaveSet") as! SearchVC
But again this will not give you the view controller that is inside your tab bar but create a new instance. And it will not instert it as one of the tabs but present a new full screen view controller.
I really love the graphical interface of the storyboard designer in Xcode, because all the segues are
displayed with an arrow.
But sometimes, when there is more complexity (e.g. variable to pass), I have to use a programatic way to switch to another ViewController.
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "HomeViewController") as! HomeViewController
// pass variable
nextViewController.email = self.textfieldEmail.text!
self.present(nextViewController, animated:true, completion:nil)
I dislike, that some of the segues in my App are displayed with an arrow in storyboard, while the more complex ones are not.
Is there any way to show those more complex segues with an arrow in the Main.storyboard?
There are two ways to achieve this:
You can create segues in interface builder and pass data in prepareForSegue method
When using code to switch controllers you can still create segue in your storyboard that you will not use - just for visualisation
In my app I use side bar as in facebook. when the user slides out the side bar a uiimageview is displayed. when user taps on the image it takes hm to a different viewcontroller. the problem i am facing is that I have created sidebar programatically and the other view to which I want to navigate the user is created using storyboard. So my source view is created programatically and destination view is created using storyboard. So can someone explain me if there is any way of using "Segue" in this scenario. Since i can not create segue using storyboard I need to do it programatically but even after a lot of googling i could not find the answer.
Well, to get another instance of another storyboard programmatically you can use something like:
let newController = UIStoryboard(name: "MyStoryboard", bundle: nil).instantiateViewControllerWithIdentifier("MyIdentifier") as! MyViewController
and then you push to your navigation controller, or add as a child view controller or something...
If you don't wanna bother with identifiers you can just use the instantiateInitialViewController instead of instantiateViewControllerWithIdentifier
Might help
"userSB" is the viewcontroller storyboard identifier
#IBAction func tapSearchCriteria(_ sender: Any?) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let aVC = storyboard.instantiateViewController(withIdentifier: "userSB") as? AViewController
aVC?.modalPresentationStyle = UIModalPresentationStyle.custom
aVC?.transitioningDelegate = self
aVC?.udelegate = self
self.present(aVC!, animated: true, completion: nil)
}
I am trying to add some unit tests to my project to test view controllers. However I seem to be having problems with seemingly simple things. I have created a sample project which I will refer to. https://github.com/pangers/ViewControllerTesting
The sample contains a UINavigationController as the initial view controller. The root view controller of the UINavigationController is FirstViewController. There is a button on FirstViewController that segues to SecondViewController. In SecondViewController there is an empty textfield.
The two tests I am trying to add are:
1) Check button title in FirstViewController is "Next Screen".
2) Check textfield in SecondViewController is empty, "".
I have heard reports of adding your swift files to both the main target and the test target is not good practice. But rather it is better to make whatever you want to access in your tests public and import the main target into the tests. So that is what I have done. (I have also set the "Defines Module" for the main target to YES as that is what I have read in a few articles aswell).
In FirstViewControllerTests I have instantiated the first view controller with the following:
var viewController: FirstViewController!
override func setUp() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
viewController = navigationController.topViewController as FirstViewController
viewController.viewDidLoad()
}
And I have added the test:
func testCheckButtonHasTextNextScreen() {
XCTAssertEqual(viewController.button.currentTitle!, "Next Screen", "Button should say Next Screen")
}
Similarly, for SecondViewControllerTest, I have set it up using:
var secondViewController:SecondViewController!
override func setUp() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
let firstviewController = navigationController.topViewController as FirstViewController
firstviewController.performSegueWithIdentifier("FirstToSecond", sender: nil)
secondViewController = navigationController.topViewController as SecondViewController
secondViewController.viewDidLoad()
}
And the test:
func testTextFieldIsBlank() {
XCTAssertEqual(secondViewController.textField.text, "", "Nothing in textfield")
}
They both fail and I am not too sure as to why. My suspicion is that the way I am instantiating the view controllers is not correct. Is the best way to instantiate the view controllers is to use the storyboard (just like it would if it were to run in real life)? Or is it acceptable to be instantiated via:
var viewController = FirstViewController()
What are you guys' experience with TDD and view controllers in swift?
I am using Swift with XCode 6.1.1.
Thanks in advance.
Solved
Ok after considering the answers from modocache and Mike Taverne, I've found my solution and I've learnt a few things which I will write down below.
1) I made anything class/method/variable that I want to test public. I do not need to add the swift files to the test target.
2) I only needed to set "Defines Module" for the "Main" target (as opposed to the "Test" target or the entire project)
3) When instantiating the storyboard, the bundle should be set to nil rather than NSBundle(forClass: self.dynamicType), otherwise tests will fail.
4) As modocache stated, it is good to give your view controller's a StoryboardID and instantiate them like so:
viewController = storyboard.instantiateViewControllerWithIdentifier("FirstViewController") as FirstViewController
However, instantiating the view controller like this ONLY instantiates the view controller alone, and not any navigation controllers that it may be embedded in. That means, attempting to do
XCTAssertFalse(viewController.navigationController!.navigationBarHidden, "Bar should show by default")
will result in a nil exception. I confirmed this with
XCTAssertNil(viewController.navigationController?, "navigation controller doesn't exist")
which resulted in a successful test.
Since I wanted to check the state of the navigation bar in FirstViewController, you must instantiate the view controller like so:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
viewController = navigationController.topViewController as FirstViewController
Now performing the test
XCTAssertFalse(viewController.navigationController!.navigationBarHidden, "nav bar should be showing by default")
results in a successful test.
5) let _ = viewController.view does indeed trigger viewDidLoad() which was confirmed by a test
6) let _ = viewController.view does not trigger viewWillAppear(), and I presume anything afterwards aswell. viewController.viewWillAppear(false/true) needs to be called manually to trigger it (Confirmed by a test).
Hopefully this will be of help to people. I will push the updated project to GitHub (link above) if anyone would like to play around with it.
Update #2
After all the above, I still could not figure out how to transition from the first view controller to the second view controller (so that I may test navigation bar properties in SecondViewControllerTests.swift). I tried
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
let firstVC = nc.topViewController as FirstViewController
firstVC.performSegueWithIdentifier("FirstToSecond", sender: nil)
secondVC = nc.topViewController as SecondViewController
which caused an error.
I also tried
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
let firstVC = nc.topViewController as FirstViewController
firstVC.toSecondVCButton.sendActionsForControlEvents(UIControlEvents.TouchUpInside)
secondVC = nc.topViewController as SecondViewController
which did not work.
I eventually tried
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
vc = storyboard.instantiateViewControllerWithIdentifier("Second") as SecondViewController
nc.pushViewController(vc, animated: false)
let _ = vc.view
vc.viewWillAppear(false)
which worked perfectly with my tests (allowed me to access navigation bar properties)!
I agree with #MikeTaverne's answer: I prefer accessing -[UIViewController view] in order to trigger -[UIViewController viewDidLoad], rather than calling it directly. See if the test failures for FirstViewController go away once you use this instead:
viewController = navigationController.topViewController as FirstViewController
let _ = viewController.view
I'd also recommend giving both view controllers identifiers in your storyboard. This will allow you to instantiate them directly, without accessing them via UINavigationController:
var secondViewController: SecondViewController!
override func setUp() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
secondViewController = storyboard.instantiateViewControllerWithIdentifier("SecondViewController")
as SecondViewController
let _ = secondViewController.view
}
Check out my talk on testing UIViewController at Brooklyn Swift for details: https://vimeo.com/115671189#t=37m50s (my presentation begins around the 37'50" mark).
I've begun unit testing view controllers recently, and it poses some unique challenges.
One challenge is getting the view to load. Looking at your set up for FirstViewController, you are trying to do this with viewController.viewDidLoad().
My suggestion is to replace that line with this:
let dummy = viewController.view
Accessing the .view property will force the view to load. This will trigger the .viewDidLoad in your ViewController, so don't call that method explicitly in your test.
This approach is considered hacky by some people, but it is simple and effective. (See Clean way to force view to load subviews early)
As an aside, I am finding the best way to test view controllers is to move as much code out of the view controllers as possible into other classes that are more easily tested.
If your view controller is defined in a storyboard, then you need to instantiate it that way for your outlets to be set up properly. Trying to initialize it like an ordinary class won't work.