Functional testing in Swift. Simulate the Application flow - testing

I'm trying to perform some really simple feature/functional testing in Swift but I have some doubts that I need to resolve to be able to create useful tests.
I want to verify that a Controller presented by another Controller exists into the Application Navigation Hierarchy (it doesn't matter if the Controller has been presented into a NavigationController, as Modal or whatever).
If I instantiate and show controllers programmatically, directly into the test functions, when I check the On Top controller I always get the Storyboard root controller instead of the controller that I have just instantiated, as if the controllers that I've manually created are never added into the Application Hierarchy.
Here an example of pseudo-code:
func testController(){
// Instantiate a controller
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controller1 = storyBoard.instantiateViewControllerWithIdentifier("Controller1") as? ControllerOneViewController
controller1.loadView()
// Call a function that instantiates another controller
controller1.pushAnotherController()
// Test that the current shown controller is what we expect...
let rootController = UIApplication.sharedApplication().keyWindow?.rootViewController
XCTAssert(rootController.self == TheExpectedClass, "Controller is not what we expect")
}

If I instantiate and show controllers programmatically, directly into the test functions, when I check the On Top controller I always get the Storyboard root controller instead of the controller that I have just instantiated, as if the controllers that I've manually created are never added into the Application Hierarchy.
From the code you wrote you are not checking the On Top controller but you are checking root view controller itself (which holds all view controllers in hierarchy including navigation controllers) so thats why you get always storyboard root view controller back. To get top most controller from view controller you can use the following recursive function which takes root view controller and return its top most controller
func topMostController(rootViewController:UIViewController)->UIViewController{
if let viewController = rootViewController as? UINavigationController{
return topMostController(viewController.visibleViewController)
}
if let viewController = rootViewController.presentedViewController{
return topMostController(viewController)
}
return rootViewController
}
and then in your testing function check the controller that this function returns
func testController(){
// Instantiate a controller
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controller1 = storyBoard.instantiateViewControllerWithIdentifier("Controller1") as? ControllerOneViewController
controller1.loadView()
// Call a function that instantiates another controller
controller1.pushAnotherController()
// Test that the current shown controller is what we expect...
let rootController = UIApplication.sharedApplication().keyWindow?.rootViewController
XCTAssert(topMostController(rootController) == TheExpectedClass, "Controller is not what we expect")
}

First you stated that it does not matter if the view is presented by a navigation controller. So i created a empty application with a navigationcontroller as initial controller and two ViewControllers, the first is just namend ViewController the second is in my case ViewControllerSecond which is your TheExpectedClass Controller.
First thing to note: If using a NavigationController obviously the rootController will always be the navigationController. So then lets check what happens if we first load the the ViewController and then within this push the ViewControllerSecond:
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controllerSecond = storyBoard.instantiateViewControllerWithIdentifier("ViewControllerSecond") as? ViewControllerSecond
controllerSecond?.loadView()
self.navigationController?.pushViewController(controllerSecond!, animated: false)
let navigationController = UIApplication.sharedApplication().keyWindow?.rootViewController as UINavigationController
let currentController: AnyObject = navigationController.viewControllers[0]
println(navigationController.viewControllers)
You will see that ViewControllerSecond was pushed to the navigationController as it should.

If I instantiate and show controllers programmatically, directly into
the test functions, when I check the On Top controller I always get
the Storyboard root controller instead of the controller that I have
just instantiated, as if the controllers that I've manually created
are never added into the Application Hierarchy.
What you're saying here is true, they aren't added. In your pseudo code all you did was instantiate some view controllers and push them onto each other.
Why are you expecting them to be in the Application Hierarchy? You never added them there.
There are actually two issues here, and that is only the first one.
The second issue:
UIApplication.sharedApplication().keyWindow?.rootViewController
This code grabs the root view controller, which is actually the one that is on the very "bottom" (assuming "top" means more visible). When you are using a Storyboard this will almost always be the initial view controller.
So even if you did add your newly instantiated view controllers to the hierarchy, the test you do will still not pass.
Suggested solution
As a simple test, you don't need to test that your new View Controller is on top the visual hierarchy. To do so you would need to add it there.
All you really need to test is - "If I push my view controller onto this newly created navigation stack, it should be at the top of that stack (visible)"
This way your test is not dependent on the application state or any other controllers that are in the hierarchy.
Pseudo code:
func testController(){
// Instantiate a controller
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controller1 = storyBoard.instantiateViewControllerWithIdentifier("Controller1") as? ControllerOneViewController
controller1.loadView()
// Call a function that instantiates another controller
controller1.pushAnotherController()
// Test that the current shown controller is what we expect...
let nav = controller1.navigationController! //Assuming it's embedded in one
XCTAssert(nav.visibleViewController.self == TheExpectedClass, "Controller is not what we expect")
}

Related

Segue between UIViewControllers without Storyboard OR XIB

I have a nifty project I downloaded from GitHub (here) and I am playing around with it. The project has no storyboard or xibs whatsoever, and only one viewController, which is defined with just a viewController.h file and a viewController.m file.
Perhaps a noob question, but can I have viewController1.h/m programmatically segue to viewController2.h/m without using ANY xibs or storyboards? I found a lot of code on SO and elsewhere allowing one to segue programmatically from one view to another within a Storyboard, from one xib to another or from a scoreboard to a xib (though not the opposite) but nothing on how to segue from one totally code-based vc to another. All the code I found requires that you define the view in terms of the bundle location of the storyboard or xib file, but I want to use neither.
Note: I accepted the answer I did because of its ingenuity/interesting-ness, but for the sake of simplicity I personally ended up opting with this answer to the same question (mine was a duplicate it appears): iOS: present view controller programmaticallly
You can use [viewController presentViewController:anotherController animated:YES completion:nil]; to present the view controller modally.
Another alternative is to use a UINavigationController and do [viewController.navigationController pushViewController:anotherController animated:YES];
The second method will only work if viewController is in the stack of a navigationController
Here is my Context class which changes view controllers. It works with either your own view classes or storyboard view classes.
Specific to your question look at the open function. If there is no root controller when I call open, I assign it as the root view controller. Otherwise I present it from the root view controller.
import Foundation
import UIKit
private let _StoryBoard = UIStoryboard(name: "Main", bundle: nil)
private let _RootWindow = UIWindow(frame: UIScreen.mainScreen().bounds)
public var ROOT_VIEW_CONTROLLER:UIViewController = C_RootViewController()
//abstract base of context classes
class Context:NSObject
{
class var STORYBOARD:UIStoryboard
{
return _StoryBoard
}
class var ROOTWINDOW:UIWindow
{
return _RootWindow
}
var _currentController:Controller!
class func reassignRootViewController(controller:UIViewController)
{
Context.ROOTWINDOW.rootViewController = controller
ROOT_VIEW_CONTROLLER = controller
}
func initController(controllerName:String)->Controller
{
return Context.STORYBOARD.instantiateViewControllerWithIdentifier(controllerName) as Controller
}
func initControllerFromStoryboard(storyboardName:String,controllerName:String)->Controller
{
var storyboard:UIStoryboard = UIStoryboard(name: storyboardName, bundle: nil)
return storyboard.instantiateViewControllerWithIdentifier(controllerName) as Controller
}
func open(controller:UIViewController)
{
if(Context.ROOTWINDOW.rootViewController == nil)
{
Context.ROOTWINDOW.rootViewController = ROOT_VIEW_CONTROLLER
Context.ROOTWINDOW.makeKeyAndVisible()
}
ROOT_VIEW_CONTROLLER.presentViewController(controller, animated: true, completion: {})
}
func close(controller:UIViewController)
{
ROOT_VIEW_CONTROLLER.dismissViewControllerAnimated(true, completion: nil)
}
}

How to create a splash page for a split view controller

(Xcode6-beta3, Swift, iPad, iOS8)
How can I create a splash page for an iPad app using a split view controller?
I've tried the straight-forward approach of drag n' dropping the little arrow to a new view controller, and setting up a button to segue to the split view controller on a touch up inside. This throws a memory error
I've also tried simply commenting out the following code from the application function in the AppDelegate, but I'm getting a
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: [identifier length] > 0'
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
// Override point for customization after application launch.
// let splitViewController = self.window!.rootViewController as UISplitViewController
// let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
// splitViewController.delegate = navigationController.topViewController as DetailViewController
return true
}
I've even disconnect the Master-Detail view in Storyboard, so that all that should be loaded is the splash page alone, but it still crashes.
I am so stuck! Thanks for your help.
The problem you were having is related to the code in application:didFinishLaunchingWithOptions:
In that code the template access the "first" view controller defined in the Storyboard to get to the split view controller and set its delegate property. If you change the "little arrow" you are changing UIWindow's rootViewController property, and being of a different view controller, it crashes.
To solve that, the best way is:
create the storyboard as described (normal ViewController with a segue to the original Split VC)
comment out code in application:didFinishLaunchingWithOptions
create a UIView controller subclass for your newly added Scene
in that class, before the segue is done, insert this modified version of the code to set the Split View Controller's delegate property:
let splitViewController = segue.destinationViewController as UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
splitViewController.delegate = navigationController.topViewController as DetailViewController
Working project here

View Controller behaves differently when set as 'initial view controller' vs. loading with presentModalViewController

My app has a map that tracks the user's location. This map will only appear under certain circumstances, and will dominate the user's attention until a particular task is complete, which is why the map isn't part of a navigation or tab bar UI.
If my map VC is set as the initial view controller in storyboard, it works fine. But if I try to load the map VC from elsewhere like this;
MapViewController *mapVC = [[MapViewController alloc] init];
[self presentModalViewController:mapVC animated:YES];
I just get a black screen.
I can confirm with NSLog that the VC is calling viewDidLoad and viewDidAppear, but the 'map' property of the VC is (null). I don't understand why (or how) I need to create the map property manually when using this technique, but it gets done for me when it is the initial VC.
The MapViewController instance in your storyboard is configured with a view hierarchy, including an MKMapView, and whatever else you did to configure that particular instance in the storyboard.
Now in this code which you show here, you are creating a completely new instance of MapViewController. It has no relationship to the instance in the storyboard other than they happen to be of the same class. So the one you create here with [[MapViewController alloc] init] has no view hierarchy (which is why you see a black screen), and none of the outlets or other configuration you may have made to the other MapViewController in your storyboard.
So what you want is to load that MapViewController that you've already set up from the storyboard. Assuming you are doing this from within a method in another view controller loaded from the same storyboard already, you can just do this:
// within some method on another vc from a scene in the same storyboard:
// given an identifier for the map view controller we want to load:
static NSString *mapVCIdentifier = #"SomeAppropriateIdentifier";
NSLog(#"Storyboard: %#",self.storyboard); // make sure this vc(self) was loaded from a storyboard
MapViewController *mapVC = [self.storyboard instantiateViewControllerWithIdentifier:mapVCIdentifier];
[self presentModalViewController:mapVC animated:YES];
And then back in the storyboard, just make sure you set the identifier for this map view controller to "SomeAppropriateIdentifier".
Hope that helps.

UISplitViewController not calling delegate methods while pushing new detailView

I setup a storyboard based on the Master-Detail Application, embed the detail view in a navigation controller, and add a new table view controller object which I will use as a second detail view controller.
I then push the new detail view controller with the following code (instead of a segue because I am pushing both a root view and a detail view controller at the same time. Only the detail view code is shown).
// Push the detailView view controller:
NewClass *newViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"test"];
newViewController.navigationItem.hidesBackButton = YES;
self.splitViewController.delegate = newViewController;
[self.detailViewController pushViewController:newViewController animated:YES];
This works perfectly, EXCEPT that the splitView delegate methods are never called before or after the push. If I do this while in portrait mode, after it pushes the detailViewController, the button to drop down the masterView popover does not show up UNTIL I rotate to landscape mode and then back to portrait mode.
How can I cause the willHideViewController/willShowViewController split view controller delegate methods to be called or manually cause them to be called?
So from what I found, it doesn't call the method because the orientation hasn't changed.
What you have to do is to pass the button from the presenting view controller since it's already tied to the popover like this:
if(self.navigationItem.leftBarButtonItem != nil) {
newViewController.navigationItem.leftBarButtonItem = self.navigationItem.leftBarButtonItem;
}
// Push the newViewController

Can't push view onto stack

I have a view controller:
#interface DetailViewController : UIViewController
It displays a map and pins are dropped onto this map. When the user tap's the accessory button one of the annotation views I want another view to be pushed in front of the user.
For some or other reason the navigation controller is always null when I run the following code.
hotelDetailViewController = [[HotelDetailViewController alloc] initWithNibName:#"HotelDetailViewController"
bundle:nil];
if (![self navigationController])
{
NSLog(#"navigation controller null");
}
[self.navigationController pushViewController:hotelDetailViewController animated:YES];
What am I doing wrong? At what point to do I need to alloc and init the navigation controller because it seems to be read only?
At what point to do I need to alloc and init the navigation controller because it seems to be read only?
Well, you don't usually set the navigationController property yourself, you would typically have a navigation controller set up from the start and then pass your DetailViewController to the navigation controller, and that's when the property is set.
The section in the View Controllers programming guide about Navigation Controllers explains how you should set up your navigation controller, either with a nib file or programmatically.