Unit Testing Initialising storyboard - ios

There are quite a few resources on unit testing whether just purely logical or application level, I am currently working trying to test a UITableViewCell that has IBOutlets within a storyboard and as a few posts have confirmed if i wish to instantiate the storyboard and then the view controller that contains the cell. So you have to go through a few steps to make sure the storyboard and following elements load properly with their IBOutlets like so
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
self.vc = [storyboard instantiateViewControllerWithIdentifier:#"testTableViewController"];
[self.vc performSelectorOnMainThread:#selector(loadView) withObject:nil waitUntilDone:YES];
followed by this code to get a UITableViewCell from the UITableViewController
[self.vc setTableViewData:#[#1].mutableCopy];
[self.vc.tableView reloadData];
[self.vc.tableView layoutIfNeeded];
SUT = [self.vc.tableView dequeueReusableCellWithIdentifier:#"More Menu Cell"
forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
and this produces a cell however for me the first breakpoint i have inside the system under test, seems to indicate that the IBOutlets have loaded, however when i actually go to assert anything, they all of a sudden disappear one me...
either references are disappearing for no reason or I'm doing something wrong.

So I have made a few discoveries which i will share here, one of them was a solution to my issue whereas originally this question was one small symptom I was attempting to fix of a larger problem.
The larger problem was, I was testing some UITableViewCells, but unfortunately their IBOutlets which where connected from the storyboard where always nil, which led me to attempt messing with how the system under test should be initialised.
I also found mixed results for using [self.vc performSelectorOnMainThread:#selector(loadView) withObject:nil waitUntilDone:YES]; as sometimes i would still have to call view or viewDidLoad for it to initialise properly. So then i did away with it entirely nd just call view and it seems to work fine.
However my cells still had nil outlets, despite my changes above. After rummaging around the internet a bit i discovered that, you should only be using [self.tableView registerClass:[MyTableViewCell class]forCellReuseIdentifier:#"MytableViewcell"]; if it hasent been created created in a storyboard as a prototype cell. It looks like it already does this for you and attempting to do it again breaks the IBOutlets.
Another small discovery was attempting to test a UIViewController of mine by setting it up like such
MyViewController *SUT = [MyViewController new];
[SUT view];
now although this initialises, again there seems to be some missing steps since this class exists in the storyboard, therefore i recommend the following
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
SUT = [storyboard instantiateViewControllerWithIdentifier:#"My Table VC"];
[SUT view];

Related

Scenes - Use dedicated Storyboards depending iPad or iPhone

Apple now wants us to use "scenes" rather than windows and screens to display content in for iPad and iPhone. Now having added the support for scenes I seem to have lost the ability to target iPad or iPhone with Storyboards?
I set my scenes inside plist like this:
This was copied from a new project, as Apple seems to have forgotten to document how to add scenes to an existing app. Another example of Apple not documenting sufficiently!
Now I seem to have lost the ability to use different storyboards for iPad from iPhone.
Whilst I could use the same storyboard for the iPad that I use with the iPhone my app looks better with the dedicated interface I have for the iPad because I use the extra real estate it offers to give a better end user experience. iPhone is fine, the interface is best suited to a small display but looks barren on an iPad.
Help!
Now I seem to have lost the ability to use different storyboards for iPad from iPhone
It's quite simple (and, as you say, not documented). You need two completely separate scene manifest entries in your Info.plist, i.e. UIApplicationSceneManifest and UIApplicationSceneManifest~ipad. They just specify different UISceneStoryboardFile values, and you're all set just as before scenes came along.
There may be a better way to do this, but searching I couldn't see anybody else covering this. Therefore, I'm giving my solution which has been accepted by the App Store here for others to review, use, improve upon, and most importantly, not waste any time on a goose chase looking for a solution that may not exist!
The way I got around this was to add another storyboard and view controller just to handle the decision if it's a iPhone or iPad, and then immediately load up the targeted storyboard. I called this "entry decision". The only issue was when closing the subsequent views you have to ensure that you're doing it correctly or you'll end up back at the "entry decision". And with a blank view, your end user could be stuck. Therefore, to ensure a user can never really be stuck I put buttons in that view so they can manually navigate should it show up if there's a change by Apple further down the line that we don't know about. Best to cover it now.
Step 1, create an new storyboard, "entry decision":
Because I'm a nice person I explain to the user it's an error and apologise. I also give them two buttons, iPad and iPhone. In theory, if all goes well, the end user will never see this, but at no cost to us it's better to cover this possibility.
Step 2, as soon as the view controller is loading get it to move to the right storyboards we actually want.
-(void)viewDidLoad {
// Need to decide which storyboard is suitable for this particular method.
// Loading from class: https://stackoverflow.com/questions/9896406/how-can-i-load-storyboard-programmatically-from-class
if ( [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad )
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPad" bundle:nil];
ViewController *detailViewController = [storyboard instantiateViewControllerWithIdentifier:#"myViewController"];
[self.navigationController pushViewController:detailViewController animated:YES];
}
else
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
ViewController *detailViewController = [storyboard instantiateViewControllerWithIdentifier:#"myViewController"];
[self.navigationController pushViewController:detailViewController animated:YES];
}
}
- (IBAction)pickediPhone:(id)sender {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
ViewController *detailViewController = [storyboard instantiateViewControllerWithIdentifier:#"myViewController"];
[self.navigationController pushViewController:detailViewController animated:YES];
}
- (IBAction)pickediPad:(id)sender {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPad" bundle:nil];
ViewController *detailViewController = [storyboard instantiateViewControllerWithIdentifier:#"myViewController"];
[self.navigationController pushViewController:detailViewController animated:YES];
}
There's nothing really special about this as it is a mix of detecting which device and then loading the right storyboards. And for full disclosure, I include a reference back to the original code I used for loading those storyboards.
The additional two methods are just the button presses, shown here for completeness.
Step 3, update the scene configuration inside plist to target Entry Decision instead of Main.
You're targeting a different storyboard, hence this needs updated.
Step 4, returning back to your main screen.
In my app, I have a welcome screen, and then table views or other views depending on which it is. Some types work find without any issues, but as I'm using a navigational controller I need to return correctly. Therefore, this one works best for the final "back" command to the main screen:
[self.navigationController popViewControllerAnimated:YES];
Other options either went back to the Entry Decision (the fault we covered just in case) or did nothing:
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:NULL];
And that is how I'm able to use my existing storyboards, which have been tweaked perfectly for iPhone and iPad over the years of this app's life, and still be able to support Apple's desire to move to scenes. My app needed to use scenes for another purpose, therefore I had no option but to go this route. I went through many attempts to try and trap the scenes as they're getting loaded, but the initial scene seems to get loaded without you, the programmer, getting any options in how it should be or which one to use. You need this pre-view controller to give you that functionality back, and it is one solution which is active in the App Store now.
Be nice.

UIStoryboard loadNibNamed analog method

I use this tutorial to achieve my custom UITabBarController: link
But I use storyboard instead of xibs. So some of method use this code below:
NSArray *nibObjects = [[NSBundle mainBundle] loadNibNamed:#"TabBarView" owner:self options:nil];
It is fair for xib but as I think not for storyboard.
This will work for u
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"StoryboardName" bundle:nil];
UIViewController *initViewController = [storyboard instantiateViewControllerWithIdentifier:#"ViewControllerName"];
Dont forget to give identifier to your viewcontroller inside the storyboard.
Note:- Make sure you have given ViewControllerWithIdentifier in storyboard.
I'm afraid storyboards are not well suited for this kind of task. There is no method for getting a specific view from a UIStoryboard instance.
In this case, I would recommend you to create a separate xib file with just the view you want to load on its own and use the code you gave to load it.
On a side note I would also not recommend using a library which has 3 years since the last commit, specially with iOS 7 out which breaks a lot of things.

Trying to initialize a view controller from storyboard

I have a view controller I placed in the main storyboard but when I try to initialize and load up the view controller my app crashes. The xcode version I'm using doesn't really tell me the errors I get properly, but I did see that it was giving me a sigabrt signal. I don't know why it isn't working.
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"SelectDateViewController"];
[vc setModalPresentationStyle:UIModalPresentationFullScreen];
[self presentModalViewController:vc animated:YES];
This is how I called it. All the names I used are correct but I still don't understand why it's crashing.
Set a breakpoint at the last line to verify vc (and also storyboard) are not nil.
presentModalViewController will crash if its viewController argument is nil.
If both are nil, then your storyboard name is likely incorrect (less likely).
If just vc is nil then the identifier is incorrect (either in code or the storyboard). Make sure the VC's Storyboard ID (as circled in the screenshot) matches your description. It is customary not to choose a class name for this (as you might have more than one instance of a given class in your storyboards).
The Storyboard ID is case sensitive and must be unique. I find the sure fire way to set it is type the name in that field after selecting the view controller and pressing return to end editing (don't just click off the field). I've noticed some corruption occurring in storyboards when converting from Xcode 4 to 5 and vice versa, so diving into the plist/xml might also help.
Edit:
Also the timing of when this is called is important, see
presentModalViewController not working.
Make sure the view controller receiving the message has already been shown (i.e. called in viewDidAppear, not viewDidLoad).
As mentioned by Abdullah, you should likely move off the deprecated method to:
[self presentViewController:vc animated:YES completion:nil];
In this case the completion block can be nil without issue, but vc must not be.
[self presentModalViewController:vc animated:YES]; is deprecated.
Try:
[self presentViewController:vc animated:YES completion:nil];
Did you set the initial ViewController from Storyboard?
If yes then Make sure the your Storyboard id is same.
If you are using storyboard you can directly present or dismiss the viewController no need to write the code for it
hope this help you :)

Unable to explicitly load view controller from storyboard

I'm loading a view controller from a story board explicitly and have this code:
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Storyboard" bundle:nil];
UIViewController *initViewController = [storyboard instantiateViewControllerWithIdentifier:#"InitialScreen"];
But am getting an error "'Storyboard () doesn't contain a view controller with identifier 'InitialScreen'"
Yet as can be seen from the screenshot, the view controller does have that identifier. I've used this identical way of loading controllers before successfully, but no idea why its not working this time. Any ideas what the issue could be?
I've just found the problem - its an issue with the simulator, I could find absolutely nothing wrong with my code and was 100% sure it was ok, when I've been in that situation in the past I usually find the culprit is Xcode itself. So even though I'd cleaned and rebuilt multiple times I decided to try resetting the contents in the simulator - and bingo it started working. I put it down to a bug in the simulator caching content and not updating to reflect changes made in IB.
Does your storyboard name match the name of the storyboard file you're trying to load the view controller from? Usually the storyboards are named something like MainStoryboard_iPhone
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
You should also check to be sure that there are no trailing spaces in the Storyboard ID textfield. Other than that everything else looks like it should work to me.

Unit testing with storyboard

Is there any way to run a specific scene from a storyboard in the simulator for testing purposes? It's inconvenient to have to tap through multiple pages in your app just to get to the right page you want to test.
It should be possible to do it:
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
UITableViewController *tableVC = [storyboard instantiateViewControllerWithIdentifier:#"MyTable"];
and if you want to simulate the view controller appearing without actually putting it on the screen:
[tableVC loadView];
[tableVC viewWillAppear:YES];
[tableVC viewDidAppear:YES];
Whether it's actually a good idea to do this is a different matter.
Unit-Tests are rather badly suited for any UI. You should try to reduce unit-tests towards models and bizz-logic, directly.
For testing a UI, aka integration tests, you might want to look at UIAutomation and/or KIF.

Resources