I'm trying to write unit tests for my view controller. It is the first view controller in my app and has a 'Account' button on the top left hand corner. Pressing this will present an action sheet, which, for now, has two buttons:
Logout
Change Passcode
I want to write tests for this functionality:
Pressing the 'Account' button should present Action Sheet.
The Action Sheet should have two buttons: 'Logout' & 'Change Passcode'.
Pressing the 'logout' button should log the user out.
Pressing the 'change passcode' button should present the passcode view controller in change passcode mode.
The problem is, if I trigger the Account button in my test, it will try to present an action sheet, which fails because the view of the controller under test is not part of a window, and that means I can't write any of the other tests either.
There are proposed solutions on testing alert views and action sheets, but they require creating a PONSO with the same interface as UIActionSheet, and doing something like this in my view controller:
// in the .h file
#property (nonatomic, strong) Class actionSheetClass;
// in the .m file
// after button is pressed...
self.actionSheetClass actionSheet = [[self.actionSheetClass alloc] init...];
This is a very unnatural way of writing code - one of those times when you twist your code out of shape just to make it testable. I get better test coverage at the expense of readability. I'd rather have my cake, eat it, and then bake some more cake and eat that too.
Does anyone know how I can test my UIActionSheet-based behaviour without resorting to such shenanigans?
Update
After Michał Ciuba's comment, I started exploring how I can use OCMock to test all the things I want to test. The problem with the approach in that thread is that the number of buttons cannot be tested, neither can their actions. The culprit is the nil-terminated argument list. Even with all the acrobatics that the Objective-C runtime makes possible, it's actually impossible to test the buttons and their actions.
This is why:
You can't show an action sheet because your tests don't have a view that is being displayed. So if the view controller uses some code like
-(void) showActionSheet
{
UIActionSheet* actionSheet = [[UIActionSheet alloc] initWith...];
[actionSheet showInView:self.view];
}
That is to say, if it doesn't hold a reference to its action sheet, you also won't be able to get a reference to the action sheet in your tests without some mocking. No reference, no checking buttons.
You can't stub the initWithTitle:delegate:cancelButtonTitle:destructiveButtonTitle:otherButtonTitles: method and check its arguments because of the nil-terminated arguments.
I tried to use Peter Steinberger's Aspects library to add an "after hook" to the that method, but the nil-terminated arguments again cause problems here because Aspects uses NSInvocation to pass the message onto the original method, which means attempting to access anything past the first item in a variable argument list will cause an EXC_BAD_ACCESS.
Swizzling the init method? That might be an option but I haven't tried yet and won't have time to for a while.
Is that really worth the effort? Certainly not, but I think it should be testable and that it shouldn't take this much effort. It's definitely been educational.
Related
I have created a custom class for my UIBarButtonItem (refreshIndicator.m). This button will be on many different view controllers, all push-segued from my MainViewController/NavigationController.
Instead of dragging an outlet onto every single ViewController.m file for iPhone storyboard THEN iPad storyboard (ugh, still targeting iOS7), I want to know if there is a way to complete my task simply within my UIBarButtonItem custom class. I've looked around everywhere but I haven't quite found an answer to this,
All I need to do is check which UIViewController is present, check the last time the page was refreshed, and then based on that time, set an image for the UIBarButtonItem. (I've got this part figured out though, unless someone has a better suggestion). How can I check for the current UIViewController within a custom button class? Is this possible?
Does it need to know which view controller its on so it can tell that vc it was pressed? If that's the case, then use your button's inherited target and action properties. On every vc that contains an instance of the button, in view did load:
self.myRefreshIndicator.target = self;
self.myRefreshIndicator.action = #selector(myRefreshIndicatorTapped:);
- (void)myRefreshIndicatorTapped:(id)sender {
// do whatever
}
More generally, its better to have knowledge about the model flow to the views from the vc, and knowledge of user actions flow from the views. Under that principal, your custom button could have a method like:
- (void)timeIntervalSinceLastRefresh:(NSTimeInterval)seconds {
// change how I look based on how many seconds are passed
}
And your vcs:
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:self.lastRefreshDate];
[self.myRefreshIndicator timeIntervalSinceLastRefresh:interval];
If you really must go from a subview to a view controller, you could follow the responder chain as suggested in a few of the answers here (but I would go to great lengths to avoid this sort of thing).
It is possible to achieve this, but the solution is everything but elegant. It is one way of getting around the basic principles of iOS and is strongly discouraged.
One of the ways is to walk through the responder chain, posted by Phil M.
Another way is to look through all subviews of view controllers until you find the button.
Both ways are considered a bad practice and should be avoided.
For your particular case, I would rethink the structure of having a separate instance of the bar button. For example, you could rework it into a single UIButton instance that gets displayed over every view controller and it can also act as a singleton.
I tried to run this method of code
- (IBAction)signInButton:(id)sender {
NSLog(#"Run Action %#", #"Here");
}
The result of this code log the "Run Action Here" twice in the console.
I initially loaded all my project import file (.m and .h) in one header file "Loader.h", I taught this was the cause, but I still experience the same issue even after I dissembled the header file.
Same Issue happens on other view controller.
What am I doing wrong ?
Thanks in advance.
It sounds like you've connected the action to your button or other UI element for two different events. For example, if you connect it to both the touch down and touch up events, a single tap of the button will trigger the action twice.
One thing you can do to diagnose the problem is to control-click on the view controller containing the action in your nib or storyboard and look at the Received Actions section near the bottom of the resulting popup. You'll likely see your action connected twice.
Another option is to set a breakpoint in the action and take a look at the sender parameter each time you hit the breakpoint. This will show you what object is triggering the action each time.
This seems to be some problem with the logging. It indeed logs twice from IBAction handlers. I put NSAlert, to make sure it's called twice, but it was called once, nevertheless the log was printed twice in the console.
I've searched through the forums and wasn't able to find anything similar to this question. It's my first time posting so please let me know if I need to add anymore information and I'll try my best!
I'm exploring Xcode and building an app for iOS 7 on an iPhone. I'm using a hypothetical purpose for the app just to see if I can learn how to build the thing (it's a booking system for taxis). It's a tabbed application (I have three tabs at the bottom corresponding to three different screens of the app, one is rates which displays a scrollable image of rates, one is a booking system that sends an email with information taken from text fields, and one is a settings page)
My questions is as follows:
On the booking page, I'd like to have a switch that either enables or disables user entry into additional text fields (it's actually for the option to book a 'return' journey, so the user can add in extra information for the return booking).
I have my page set up with the first text fields in place, but I can't for the life of me figure out or find anywhere about how to make this switch enable entry into the additional text fields. Ideally I'd like them to be greyed out and disabled if the switch is off, and enabled if the switch is on.
Any help on the matter would be much appreciated!
Thanks.
edit:I'm also doing this with storyboard, wasn't sure if this made a difference!
You can use UISwitch and UITextField for this. UITextField has a property called enabled that controls whether the user can interact with it.
First you need to create an IBOutlet for your UITextField in storyboard by control-dragging it to respective #interface definition in your header file. Then you need to create an IBAction for your UISwitch, again by control-dragging it to #interface (choosing 'Action' for 'Connection' and 'changed' for 'Event').
Finally implement the newly generated method like this:
#implementation ViewController
-(void)mySwitchChanged:(id)sender
{
UISwitch *mySwitch=(UISwitch *)sender;
myTextField.enabled=[mySwitch isOn];
}
.
.
What ways are there to change views other than using a navigation-based app? I want my first view to be basically just a simple form and when the user hits a "submit" button I want to send him over to the main view, without being able to return. And I don't want the bar on the top of the view either.
How can I achieve this, and if possible without losing the animations that come with a navigation-based app.
If you wanted your app to be entirely navigation controller free, you can use one of the signatures of presentModalViewController:animated: from whichever UIViewController you deem best fit to be the parent. Call [self dismissModalViewControllerAnimated:YES] on the child view (form you want submitted) after you've handled state change on submit. One thing to watch out with this, is as of iOS 5, Apple now prefers that you use presentViewController: instead, and presentModalViewController: is marked for deprecation at a future date.
In terms of "how you would know the user submitted the form, so they may now proceed in your application" - one way you could do that is to use delegation / notifications to maintain awareness of the state of the form. When the child form is submitted, you can call the parentViewController's delegate callback to set flags - or return authentication data, for example - in your AppDelegate or some high-level class. Delegation and Notifications are useful tools when using the iOS SDK.
Option two could be using a completion handler in with your call to present the child, such as:
ChildForm *childFormWithSubmit = [[ChildForm alloc] init];
[self presentModalViewController:childFormWithSubmit animated:YES
completion:^(/*inlineFunctionParams*/)
{ /*inlineFunctionBodyToRunOnCompletion*/ }];
Lots of possibilities ~
Did you look at 'presentViewController:animated:completion:' in the UIViewController class description? There are lots of options for how you animate in another viewController.
Sled, you can simply just hide the UINavigationBar for your UINavigationController.
That way you won't see the UINavigationBar and the user will not be able to return back to that page.
You'll need to set a permanent flag in your app either writing to text file or using NSUserDefaults.
I've got an app that I've developed for the iPhone, but now want to port to the iPad. The iPhone app is navigation style and based on discrete table view controllers managed by a nav controller. The larger screen real estate of the iPad means that I can comfortably fit a couple of these table view controllers on to the screen at the same time.
The question is how? Should I
a) have the main view load two table view controllers from separate NIBs and then position them on screen (I'm not sure how I set they x and y of subviews loaded from nibs).
b) create sub-views in my main nib and populate these with data from my existing classes (if so how do I hook up the IBOutlets)?
c) do something completely different
One thing I should point out is that I don't want to use the split screen option.
Alert! This QA is now of historic value only.
It is now trivial to do this sort of thing with container views in iOS, which is why Apple edited them:
https://stackoverflow.com/a/25910881/294884
How to add a subview that has its own UIViewController in Objective-C?
Historic answer...
".. how I set they x and y of subviews loaded from nibs?"
I'm not sure if I fully understand your question Phil, but here's an easy and clear way:
Fire up interface builder and in the new larger iPad view, simply add new smaller views (UIViews)... Put them exactly where and how you want them. We are going to call these "basket" views.
Let's say one of your complicated views from the other app is your fatDogs view. Call the new basket view fatDogsBasket. Then in the code, in viewDidLoad, just do the following with all these "baskets"...
[fatDogsBasket addSubview:fatDogs.view];
[clientsBasket addSubview:clients.view];
[namesBasket addSubview:names.view];
[flashingLightsBasket addSubview:flashingLights.view];
// etc
You're done! (Obviously, make sure that the relevant view controllers, fatDogs, flashingLights and so on, are all ready to go and instantiated.)
The "basket" system is handy since each one will hold your previous work in one place; usefully you can (say) set overall invisibility or whatever just by touching the baskets. Obviously, if you want to set, or maybe move, the position of a basket in the code, just go
happyBasket.frame = CGRectMake(509,413,
happyBasket.frame.size.width,
happyBasket.frame.size.height);
UIViews in iOS are very lightweight, so it's no problem at all adding another layer of UIViews.
I hope this is what you were getting at!
------Later...
You went on to ask: "Just to make sure I'm clear on the right way to implement this. The main view controller has IBOutlets for each of the 'baskets' and its this IBOutlet connection to the subview that I'm calling. Each of the view controllers that I'm going to show in each basket has it's own nib and associated IBOutlets. Right? –"
So, "The main view controller has IBOutlets for each of the 'baskets'"...
Right, the main view in the new app, would have lines like this in the .h file:
IBOutlet UIView *fatDogsBasket;
Note that you are simply declaring "fastDogsBasket" to be a UIView. You shouldn't worry too much about the "IBOutlet" word. All that means is "I need to be able to look this item up, over in the interface controller." It's important to realise IT DOES NOTHING.
So yes all the "baskets" will be UIViews and hence of course you must delare them as such in the .h file of your main view controller. Personally is would not use the phrase "a view controller has IBOutlets." It sort of confuses things and gives the wrong idea. Just say "don't forget to mark the UIViews as iboutlets in the header file."
So anyway yes that's exactly what you do, declare all the "basket" UIViews in the .h file of the main controller, and indeed mark them all as IBOutlets so that interface builder will work more easily. Next ..
"its this IBOutlet connection to the subview that I'm calling" -- that's wrong.
The basket such as fatDogsBasket IS SIMPLY A UIVIEW and that's that. It's just a UIView.
Now, as you know you can put UIViews inside other UIViews. (Obviously, this is commonplace, every UIView has scores of UIViews inside it and so on and on - it's the most basic part of building up an interface.)
So, what are you going to put inside your fatDogsBasket uiview? You're going to put in ALL YOUR PREVIOUS WORK on fatDogs! Previously (for the iFone) you wrote a wonderful class - a view controller - called fatDogs. (It may well have even had many subclasses and so on.)
We're now going to take the view from fatDogs (of course, that is fatDogs.view) and literally put it inside fatDogsBasket. (Recall that fatDogsBasket is a UIView.)
So firstly you would have to completely include your amazing class fatDogs (from the old project) in your new project. Click "add existing flies/classes" or something like that...you'll figure it out. Yes, add all the class files, xibs, any subclasses and so on.
Then, simply do this .. in your new super-powerful uber-controller, in viewDidLoad, just do the following with all the "baskets"...
[fatDogsBasket addSubview:fatDogs.view];
[clientsBasket addSubview:clients.view];
[namesBasket addSubview:names.view];
[flashingLightsBasket addSubview:flashingLights.view];
// etc
You're done! Note that the view from fatDogs (ie, fatDogs.view) is now displaying inside of the UIView fatDogsBasket. The class fatDogs will now work completely normally, just as it did in the old days! And incredibly, you can easily (here in your new controller) do things like simply move fatDogsBasket, and it will move the fatDogs view easily all at once, without worrying about the details of fatDogs and it's views.
Finally you ask..
"Each of the view controllers that I'm going to show in each basket has it's own nib and associated IBOutlets."
Exactly correct. When you add your old system "fatDogs" to the new project, you will be adding all of it's xib files and so on. Anyting that happens or doesn't happen inside those classes, to do with perhaps buttons or anything else marked as iboutlets, or anything else, will just still be the same within those classes. I'm pretty sure absolutely NOTHING will change when you use those old classes in your new project.
Just for the record .. "Each of the view controllers that I'm going to show in each basket.." Just to be accurate, you don't really show as such a viewcontroller, you show the view of the viewcontroller (!!). In other words, for fatDogs (a view controller) you will be showing it's view, which is, simply enough, referred to as fatDogs.view. ("view" is, of course, a property of any UIViewController, so you can just say vcName.view and you're done.)
Hope it helps!
And finally you ask .................................
"I've got it compiling OK, but my baskets are showing up empty, i.e. they're not showing the views of the view controllers that I've imported."
Tell is the name of one of your UIViewController classes from the old project, so we can be specific
Let's say you have an old UIViewController called HappyThing. So you will very likely have a file HappyThing.h and a file HappyThing.m and a file HappyThing.xib.
put all those in the new project, you must do so using Add->Existing Files. (Control on one of your current filenames in the list on the left in XCode.)
You will have to do this #import "HappyThing.h" somewhere or other in your new project - either in your Prefix.pch file or at the top of your new UIControllerView
To be clear in HappyThing.h you will have a line of code
#interface HappyThing : UIViewController
In your new UIViewController.h file, you will have to add a new variable, we'll call it xxx,
HappyThing *xxx;
Note that the type of xxx is HappyThing. (Note that as a rule, you would use the naming convention "happyThing" (note the lowercase "h") rather than "xxx", but it's just a variable and I want it to be clear to you that it's just a variable.)
Next! At the moment it's just a variable that is not pointing to anything, it's nothing. (Just as if you said "int x", but then did not actually say "x = 3" or whatever.) So! In your code you have to actually instantiate xxx.
xxx = [[HappyThing alloc] init];
[xxxBasket addSubview:xxx.view];
Note that the first line is what makes an instance of HappyThing come in to existence. And of course, you want to use "xxx" to point to that instance.
The second line puts the view in to the relevant basket! Note that of course what you want is the view associated with xxx (ie, xxx.view) ... remember that xxx is a UIViewController, it is not itself a UIView. The associated UIView is "xxx.view". (The view is literally just a property of xxx.)
Memory management! Note that you used "alloc" to bring xxx in to existence. This means you DO own it, and of course that means YOU DO NOT need to send a retain there. Furthermore, since you do own it, that means You eventually have to RELEASE it. (easy ... [xxx release];)
So simply add the line [xxx release]; to the dealloc routine in your new UIViewController. (Really it won't cause any harm if you forget to do this, but do it anyway.) Conceivably you may want to release it earlier for some reason once you are more comfortable with the process.
(I was just working on a project with a huge number of huge tables, popovers and the like, so I only made them on the fly and got rid of them as soon as possible, to use less memory. But all of that is irrelevant to you at this stage.)
So now you should SEE IT ON THE SCREEN!
Don't forget if you previously had some routine in HappyThing, which you had to call to start it working (perhaps "beginProcessing" or something), you'll have to call that yourself from the new UIViewController. Hence perhaps something like:
xxx = [[HappyThing alloc] init];
[xxxBasket addSubview:xxx.view];
[xxx beginProcessing];
[xxx showAmazingRedFlashingLights]; // or whatever
Finally you asked ...
"When you've use this technique, do you simply include the headers of the imported files in your main view controller, or do you forward class them in some way?"
That was not your problem, your problem was that you were not instantiating it with the line xxx = [[HappyThing alloc] init];. So, good luck!
Regarding the line of code "#class HapyyThing", if you want to simply put it just above the start of the definition of your new UIControllerView. Generally you don't have to if you have your include line in the best place. Anyway it is an unrelated issue. It simply won't compile if your new UIViewController, can't find HappyThing. Enjoy!