Set delegate of viewcontrollers in UITabbarController - ios

I have the following setup in my app:
A UITabbarController with 3 viewcontrollers, with embeded UINavigationControllers.
The 3 viewcontrollers inheret/superclass from a UIViewController subclass called "SVC", in order to implement elements which is used in all of the 3. viewcontrollers and prevent duplicated code. In this "SVC" class I have setup a delegate called "dismissDelegate" (which is used to tell when the tabbarcontroller is dimissed).
#protocol ModalViewDelegate <NSObject>
- (void)didDismissModalViewFrom:(UIViewController *)viewController;
#end
#property (weak, nonatomic) id <ModalViewDelegate> dismissDelegate;
My other viewcontroller which segues to the UITabbarController, implements this delegate in order to get information about, when the tabbarcontroller is dismissed.
the SVC class notifies the delegate of dismissal of the tabbar like so:
[self.dismissDelegate didDismissModalViewFrom:self];
I now want to set the delegate of all the viewcontrollers which inherts from the SVC class (all the tabbar viewcontrollers) to this viewcontroller and I try to do this via the prepareToSegue method like so:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"ToTab"]) {
UITabBarController *tabBarController = segue.destinationViewController;
for (UINavigationController *navController in tabBarController.viewControllers) {
for (UIViewController *vc in navController.viewControllers) {
_SubclassVC = (SVC *) vc.superclass;
_SubclassVC.dismissDelegate = self;
}
}
}
}
But I get the following error:
+[SVC setDismissDelegate:]: unrecognized selector sent to class 0xbca68
My questions:
Is this the right way to tackle this senario (get information about dismissal of a viewcontroller and setup this delegate in a subclass which is inhereted by multiple viewcontrollers)?
How do I manage to set my first viewcontroller as the delegate of all the viewcontrollers in the tabbar - the SVC class, so I can get notified when the tabbarcontroller is dimissed and solve the error?

+[SVC setDismissDelegate:]: unrecognized selector sent to class 0xbca68
See the +
The plus sign idicates that you are calling a class method. You must have tried setting a class variable by a setter. But a property represents instance variables only. Therefore the setters and getters that are automatically generated are intance methods only. (starting with a minus - in error messages like this).
And that is what you do:
_SubclassVC = (SVC *) vc.superclass;
_SubclassVC.dismissDelegate = self;
For whatever reason (probably by mistake or misunderstanding) you take the vc instance and get its superclass. vc.superclass returns a class object, not an object (meaning not an instance, in Obj-C class objects are objects too).
Then you typecast it to (SVC *) just to stop the compiler from throwing errors (or warnings - not sure).
Well, I guess that you wondered yourself why you have to typecast it at all. That's the reason :)
Next, you assign self to a property dismissDelegate. The compiler does that because you typecasted it to SVC* which does have a property dismissDelegate. The compiler will actually call the setter setDismissDelegate as usual in contructs like this.
BUT at runtime the message (or selector) setDismissDelegate: is not sent to an SVC* but to a class object. And the class SVC does not have a method (or selector) +setDismissDelegate: and therefore cannot resolve the message. And that is exactly what the error message is telling you.
All right, now we get to your questions.
1. Well, it is not the way I would do it, but that is certainly one way of achiving it.
2. If you want to stick with that unusual approach then do this minor change and you will get rid of the error:
for (SVC *vc in navController.viewControllers) {
vc.dismissDelegate = self;
}
There is no point in fetching the superclass object. If you cannot access the property of a superclass then you did something wrong with the inheritance chain.
If you want to be on the save side:
for (UIViewController *vc in navController.viewControllers) {
if (vc isKindOfClass:[SVC class]){ //BTW, this is good usage of the class object
SVC *svc = (SVC*) vc;
svc.dismissDelegate = self;
}
}

Related

How to understand/create a delegate to dismiss a view controller

Trying to get a better understand of iOS delegation. I'm using UIImagePickerController as a reference but what's a good code example to use a delegate to dismiss my view controller?
I have a TabBarViewController that calls AViewController and want to use delegation to dismiss AViewController.
In this case, the idea is that the class that presented the new viewcontroller, should also be the one that dismisses it. The AViewController may have no clue how it was presented, so it wants to let the presenter, the TabBarViewController, handle the dismissal in whatever form needed. So, we need to define a protocol, say AViewControllerProtocol, which allows there to be a standard definition of the dismissal call:
This goes in AViewControllerProtocol.h:
#protocol AViewControllerProtocol <NSObject>
#required
- (void) dismissWithDone:(UIViewController *)viewController;
- (void) dismissWithCancel:(UIViewController *) viewController;
#optional
- (void)dismissWithDone:(UIViewController *)viewController andPlaySoundFile:(NSString *)soundPath;
#end
This goes in a file called AViewControllerProtocol.h. Both TabBarViewController.m and AViewController.m will import it. Think of this as a pact that any user of AViewController must agree to before it can utilize it. Similarly, you can't use UITableView without making a pact to observe the UITableViewDelegate and UITableViewDataSource protocols (those are two separate protocols).
Any class which wants to present AViewController, can see from the protocol definition that AViewController expects two required methods in order to be properly dismissed. It also has an optional third method that requests the presenter to play a sound after dismissing.
In order for AViewController to do its piece of this pact, it needs to get and store one piece of information about who the presenter is. That is called the delegate. The delegate is a property defined in the #interface of AViewController, in AViewController.h:
This goes in AViewController.h:
#property (nonatomic, weak) id<AViewControllerProtocol> delegate;
Now, the presenter, TabBarViewController, needs to do its bit. It needs to define the two required methods, plus maybe the optimal one, and it also needs to set the delegate value:
In TabBarViewController.m, in the #implementation:
This goes in TabBarViewController.m:
- (void) dismissWithDone:(UIViewController *)viewController
{
[self saveData:viewController.dataToSave]; // this could be the results that need to be saved
[viewController dismissViewControllerAnimated:YES completion:^{
;
}];
}
- (void) dismissWithCancel:(UIViewController *)viewController
{
// don't save data
[viewController dismissViewControllerAnimated:YES completion:^{
;
}];
}
The delegate value is set where AViewController is first created and/or before it is presented:
This also goes in TabBarViewController.m:
AViewController * aVC = [AViewController.alloc init];
aVC.delegate = self;
aVC.data = ...; // this may be the data you want changed by the VC
[self presentViewController:aVC animated:YES completion:^{
}];
Setting the delegate here is the only connection that the AViewController class has with its presenting viewController - which is the whole point here: child classes really shouldn't have to know a whole lot about the classes that utilize them.
Lastly, the AViewController class needs to add the following in order to call, via the delegate, back to the presenting class - so in AViewController.m:
This goes in AViewController.m:
-(IBAction)userHitButton:(id)sender
{
if (sender == doneButton) {
if ([_delegate respondsToSelector:#selector(dismissWithDone:)]) {
[_delegate dismissWithDone:self];
}
} else {
if ([_delegate respondsToSelector:#selector(dismissWithCancel:)]) {
[_delegate dismissWithCancel:self];
}
}
}
If you are wondering why the calling class has to:
aVC.delegate = self;
It is because there are situations where the class that actually creates a child class, isn't the one that will handle the delegate calls. In that case, instead of self, you put an instance of a class that will handle the delegate callbacks. For example, lets say that you have AViewController, BViewController and CViewController. They all get data from the user that needs to be saved. A class by the imaginary name of ABCDataHandler could be the one that can handle the dismissWithDone: and dismissWithCancel: callbacks and saves the data as needed, and TabBarViewController can stay out of any data handling activity.
That's to put it as simply as I can. I hope I haven't made any typos here :-)

Objective C - Access Method befor changing View Controller

I'm trying to call a method before I change the ViewController. Here's my Code:
ViewController.m (First View, here I want to set a String)
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"loginCorrect"]){
[segue.destinationViewController setMail:#"asd"];
}
}
ViewControllerMainMenu.h
- (void)setMail:(NSString*)mail;
#property (strong) NSString *userMail;
ViewControllerMainMenu.m
- (void)setMail:(NSString*)mail
{
self.userMail = mail;
}
As you can see, I want to use the userMail String in the second View, which I get in the first View (a classic Login should be the result).
But I always become this Error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UITabBarController setMail:]: unrecognized selector sent to instance 0x147635080'
Hope you can help me, Thanks!
Emanuel
You need to take reference of YourViewController
UITabBarController *tabbar=[segue destinationViewController];
// i am assuming YourViewController at index 0
YourViewController *vc=(YourViewController *)[tabbar.viewControllers objectAtIndex:0];
[vc setMail:#"asd"]
YOu are calling a method on tabbarcontroller which is rely on viewcontroller, so call it properly to reach your goal
It looks like you are sending a message to the object that can not handle this. This is caused because you think you are sending it to the right object but in fact (in runtime) it is not. You should debug and see what is exactly the problem. It might be the retrieving the object you are sending a message or you should have some check if that sending a message should occur in fact - all that depends what you want to achieve. Maybe you will need some casting to help compiler figure out whit what to deal with
I'm guessing your segue destination is a UITabBarController and in it you have a custom UIViewController? We'll need to see proper code in order to answer properly but here's my best guess for now.
First you need to import your class...
#import "ViewControllerMainMenu.h"
Then in prepareForSegue...
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"loginCorrect"]){
UITabBarController *tabBarController = segue.destinationViewController;
ViewControllerMainMenu *controller = tabBarController.viewControllers[0];
controller.mail = #"asd";
}
}
Obviously, this depends on exactly how everything is set up but you should be able to adapt it from here.

Call method for different UIViewController

I am making master detail application, i have dynamic Detail ViewController. Detail ViewController are changed.
But in every Detail ViewController I have one common method updateInfo I want to call that method
Here is my code
UINavigationController *nav=[self.splitViewController.viewControllers objectAtIndex:1];
UIViewController *controller=[nav.viewControllers objectAtIndex:0];
[controller updateLastInfo];
But it gives me error no method found.
it will work if i use UIViewController name.
HomeViewController *controller=(HomeViewController)[nav.viewControllers objectAtIndex:0];
[controller updateLastInfo];
But i dnt want to do above things.
I have tried to explain. Please help
You can use id
UINavigationController *nav=[self.splitViewController.viewControllers objectAtIndex:1];
id controller=[nav.viewControllers objectAtIndex:0];
[controller updateLastInfo];
You could subclass UIViewController and make a base DetailViewController class that houses common functionality of your detail view controllers. Then you would make all of your detail view controllers subclass DetailViewController instead of UIViewController. This would be a safe way to do it and would also allow you to add extra functionality to your updateInfo method in the specific detail view controllers.
If you want an unsafe way, you could make your controller object of type id. I wouldn't suggest this approach as it relies on your personal knowledge of the code. If someone else (or yourself down the road) sets it to a view controller that doesn't have that method, the code will still try to run and will crash.
UIViewController doesn't have a method named updateInfo, so the compiler will of course complain when you try to send that message to a pointer that's known only to point to an instance of UIViewController. When you use the class name, like this:
HomeViewController *controller=(HomeViewController)[nav.viewControllers objectAtIndex:0];
you're providing more information to the compiler, using a type cast to tell it "Hey, don't worry, I know for certain that the object I'll get back is a HomeViewController. Since you seem to have several types of view controllers that all have this method, the best thing to do is to declare the updateInfo method in a protocol and then have each of those UIViewController subclasses implement that protocol. So, your protocol declaration would be in a header file and might look like:
#protocol SomeProtocol
- (void)updateInfo
#end
and each class that has an -updateInfo method would just need to declare that it adopts the protocol:
#interface HomeViewController <SomeProtocol>
//...
#end
and then make sure that you have an -updateInfo in your class implementation:
#implementation HomeViewController
- (void)updateInfo {
//...
}
//...
#end
Then, in your code, you can either check that the object conforms to the protocol using -conformsToProtocol: like this:
if ([controller conformsToProtocol:#protocol(SomeProtocol)]) {
UIViewController<SomeProtocol> *c = (UIViewController<SomeProtocol>*)controller;
[c updateInfo];
}
or else just check that the object responds to the selector before calling it:
if ([controller respondsToSelector:#selector(updateInfo)]) {
[controller performSelector(updateInfo)];
}
The other answers you've received (using id or creating a common base class) are also good ones, but to be safe make sure you do some checking before calling your method. For example, you can use -isKindOfClass to make sure that the view controller you get back is in fact an instance of your common base class, and you can use -respondsToSelector: as above to check that an id points to an object that implements updateInfo.

iPhone - Call function from another view controller

I have a function called sendDataToMotor. It is in my First View Controller classes. I have another view controller called SecondViewController. I need to call this function from the Second View Controller.m class. I tried declaring the property:
#property(nonatomic,assign)UIViewController* firstController;
in my SecondViewController.h class. Furthermore, I wrote the code bellow in the viewDidLoad part of my SecondViewController.m class (where I want the function to be called).
secondViewController = [[SecondViewController alloc] initWithNibName:#"secondViewController" bundle:nil];
secondViewController.firstController = self;
[self.firstController performSelector:#selector(sendDataToMotor)];
But, Im getting an error with the first word in that code (secondViewController) because of an undeclared identifier issue. Furthermore, I get an error with the second line (secondViewController.firstController = self) because secondViewController has an unknown name type.
In summary, I don't care if you use the above code to answer my question: that was just something I tried to implement that I found online. However, I'm looking for the simplest way to call a function from another View Controller.
Notification Center could be solution to you question.
Receiver UIViewController
- (void)viewDidLoad {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(receiveNotification:)
name:#"myNotification"
object:nil];
}
- (void)receiveNotification:(NSNotification *)notification
{
if ([[notification name] isEqualToString:#"myNotification"]) {
//doSomething here.
}
}
Sender UIViewController
- (void)sendNotification {
[[NSNotificationCenter defaultCenter] postNotificationName:#"myNotification" object:self];
}
You want to use the delegate pattern, and you are almost there.
This line in secondVC:
#property(nonatomic,assign)UIViewController* firstController;
should be generalised so as not refer to a specific type
#property(nonatomic,weak)id <delegateProtocol> delegate;
And you should accompany that with a protocol declaration in the header of your secondVC (above the #interface declaration), something like
#protocol SecondVCDelegate
- (void) sendDataToMotor;
#end
In firstVC interface you can declare your adherence to the delegate protocol in the first line of the #interface declaration in the header file
#interface firstVC < SecondVCDelegate >
Or in the first line of a private interface declaration in the .m file
#interface firstVC() < SecondVCDelegate >
Then you won't need to use performSelector (which anyway should be preceded by a safety check) as the compiler will alert you of errors.
In firstVC after creating secondVC, set it's delegate property to self(i.e. firstVC)
secondVC.delegate = self;
then in secondVC you can just call the method directly on it's delegate:
[self.delegate sendDataToMotor];
I explain this in more (wordy) detail here...
https://stackoverflow.com/a/14910469/1375695
There are many problems in your code. I'm going to assume the second chunk of code you included is actually in -viewDidLoad in FirstViewController not the second.
The error you're getting is because you aren't putting the type before secondViewController. It should be SecondViewController *secondViewController = ...
This probably still won't work for you because when you perform your transition to the second view controller, you won't be using the same object.

Delegate is nil

I have a property on a ViewController which I set from a parent SplitViewController:
Property declaration/synthesization
#interface DownloadRecipesViewController_iPad : DownloadRecipesViewController<PopoverMenuDelegate, RecipeChangeDelegate>{
id <NSObject, PopoverMenuParentDelegate, RecipeChangeDelegate, RecipeDownloadedDelegate> _selectionDelegate;
}
#property (strong) UIBarButtonItem *btnMenu;
#property (strong) id <NSObject, RecipeChangeDelegate, RecipeDownloadedDelegate> selectionDelegate;
#implementation DownloadRecipesViewController_iPad
#synthesize btnMenu;
#synthesize selectionDelegate = _selectionDelegate;
I wire up the delegate in the parent SplitViewVC's viewDidLoad method:
Wiring up the delegate
self.downloadVc = [self.storyboard instantiateViewControllerWithIdentifier:#"RecipeDownload"];
[self.downloadVc setSelectionDelegate:self];
A button in the Child VC calls a method to fire an event up to the parent ViewController, but when this event is called, the delegate is nil and the event isn't fired. I've wracked my brains trying every which way to find out why this happens but I'm at a total loss.
Delegate is nil here (firing the delegate):
-(IBAction)didTapMenu:(id)sender{
if([_selectionDelegate respondsToSelector:#selector(shouldToggleMenu)])
[_selectionDelegate shouldToggleMenu];
}
I've also tried without the backing property but hit the same problem.
Suggestions on how to find this follow. But first, why not save yourself some typing and remove the ivar and remove the #synthesize - its totally unnecessary typing at this time. Also, as a comment said, delegates should almost always be typed as weak.
Suggestions:
1) Write a (temporary) setter for the selectionDelegate, then set a break point where you actually set the value (or after) so you can verify that its getting set, and that nothing else is zeroing it out.
2) Set a breakpoint on the IBAction method, on the line where the if statement is, and when you hit it verify the object is the same one where you set the delegate, what the delegate value is, and then see if the respondsTo method succeeds (use single step).
The way I eventually solved this was:
Create a new delegate which exposes a method: -(void)segueBeginning:(UIStoryboardSegue*)destination
Use this delegate to expose prepareForSegue from the child UINavigationController to the parent SplitViewController
.3. Hook up my child VC in the parent ViewController when prepareForSegue is raised in the child nav controller:
if([destination.destinationViewController isKindOfClass:[DownloadRecipesViewController_iPad class]] && !self.downloadVc){ // download recipes
self.downloadVc = destination.destinationViewController;
self.downloadVc.selectionDelegate = self;
[self.downloadVc setSnapshotChangeDelegate:self];
[self.downloadVc.navigationItem setRightBarButtonItems:[NSArray arrayWithObjects:btnAdd, nil]];
}

Resources