I implemented a custom UIViewController which has multiple child view controllers (mainly by using the storyboard's Container View but not only).
I'm wondering what is the best way for the "root" view controller to send a message to its children, taking into account that the child(s) view controller(s) concerned by the message sent is not necessarily a direct child of the "root" view controller ?
For example:
I want to send a message from #0 to view controllers #1 and #4. The naïve implementation is to iterate over the child of #0 and send them the message, like so:
for (UIViewController *childVC in self.childViewControllers)
{
if ([childVC respondsToSelector:#selector(myMessage:)])
{
[childVC performSelector#selector(myMessage:)];
}
}
But it simply doesn't work, because #3 will receive the message (or probably not if it is a UINavigationController and I'm calling a custom method) and don't propagate it to its children (here #4).
So is there a possibility to send a message to one's children, and let the message propagate through the UIViewController hierarchy ?
If not, an alternative would be to use the NSNotificationCenter, make the children interested in the message (#1 and #4) observe for a notification, and make the root view controller post a notification when necessary. But I find it a bit of an overkill only to dispatch a message to two children.
I tend to avoid NSNotificationCenter, because if one is not careful it will create a bit of Spaghetti Code. Still, in this case I would use it, because it's even more of a overkill to taking of the logic of how the message should propagate between the childsViewControllers and possibility the ochildsViewControllers's childs.
I think going through NSNotificationCenter is the best way to go. Each child view controller can decide for themselves what notifications they want to respond to; it takes the responsibility of filtering away from the object posting the notification.
NSNotificationCenter
And its Guide
You could also do something like this, but I'm not saying its a good idea:
#interface UIViewController (ReverseResponder)
- (void)reverseRespond:(SEL)selector withObject:(id)object;
#end
#implementation UIViewController (ReverseResponder)
- (void)reverseRespond:(SEL)selector withObject:(id)object
{
if ([self respondsToSelector:selector]) {
[self performSelector:selector withObject:object];
}
for (UIViewController* childViewController in self.childViewControllers) {
[childViewController reverseRespond:selector withObject:object];
}
}
You can access all of your children with self.childViewControllers, and pass a specific index to get to a particular child. To get to 4, you'll have to access 3's topViewController.
UIViewController *child4 = [(UINavigationController *)self.childViewControllers[2] topViewController];
Whether this is a good way to do this, depends on what you're trying to do. Why are you sending messages down to this level?
Related
Passing data between two view controllers seems to have been solved using delegates. My situation is little different and since I am new I don't know if I can solve this with delegates.
I have 3 view controllers. GrandParent, Parent and Child.
GrandParent instantiates Parent that shows list of CategoryGroups.
Clicking on a CategoryGroup instantiates Child View Controller that shows list of Categories.
I want that when user clicks on any Category, GrandParent gets to know the Category being clicked.
What I have now?
On Child.h view controller
#protocol CategorySelectDelegate<NSObject>
- (void) categorySelected:(CategoryModel *) categoryModel;
#end
On Child.m view controller
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"selected category:%#", _categories[(NSUInteger) indexPath.row]);
[self.delegate categorySelected:_categories[(NSUInteger) indexPath.row]];
[self dismissViewControllerAnimated:YES completion:nil];
}
On GrandParent.h
#interface GrandParent : UIViewController<CategorySelectDelegate>
On GrandParent.m
- (void)viewDidLoad {
[super viewDidLoad];
ChildViewController *categoryViewController = [[ChildViewController alloc] init];
childViewController.delegate = self;
}
- (void)categorySelected:(CategoryModel *)categoryModel {
_categoryLabel.text = categoryModel.name;
NSLog(#"categoryLabel:%#", _categoryLabel.text);
}
But I know this is incorrect since GrandParent is not the one instantiating Child directly, its is always parent who gives birth to Child
Question
- How can I pass the categoryModel from Child to GrandParent?
- In general, how can I pass a data from one Child Controller back to any Ancestor Controller?
UPDATE
For now, I have added 2 delegates to solve this problem
a.) 1 delegate from Child to Parent
b.) 1 delegate from Parent to GrandParent
This works but I don't think it is a good design is data needs to be passed between 2 or more view controllers since one will end up creating new delegates to pass values.
I had more or less same use case, and i preferred to go with the notification, as its seems to be loose coupled object,
making delegate just for exchanging data would not be a good choice.
Please refer How Best to Use Delegates and Notifications which says,
Notifications result in loose coupling between objects. The coupling is loose because the object sending a notification doesn't know what is listening to the notification. This loose coupling can be extremely powerful because multiple objects can all register to listen to the same notification
So down the line, if some other view controller or any other widgets wants to handle data, it can be achieved easily without setting one more delegate.
but this line also holds good
The fact that notifications and delegates provide such different coupling are an indicator that they should be used in different scenarios. If table view used notifications instead of a delegate, then all classes that use a table view could choose different method names for each notification. This would make it hard to understand code as you would need to go and find the notification registration to work out which method is called. With a delegate, it's obvious: all classes that use a table view are enforced to be structured in the same manner.
Interesting problem you have.
You can establish a set of global protocols that can be subscribed to by any object and pass around who receives the messages. This can be as easy as building a separate .h
So, as the parent builds the child, the parent must set the grandparent.delegate = child before presenting that child view controller.
And then of course as the child is removed and the parent shown again, the delegate needs to be set back.
If you want to use delegate then there is no way but propagating the GrandParent to Child as a delegate so that it can send callback to GrandParent when category is selected.
Alternatively you can post NSNotification from child when category is selected and add GrandParent as a observer to get the notification.
When using a UINavigationController, when the user is "diving in deeper" (pushing yet another controller on the stack), I have an opportunity to handle that in
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
But how do I handle the opposite? When the user presses the back button, and a controller becomes the top controller again, I'd like it to potentially update some state because the controllers on the stack may have changed some things I want to reflect in the now visible controller.
Or, by analog, when I use modal segues to present new controllers, I get to pick a method that is called as an unwind segue when the presented controller exits. How can I do the same with navigation stack managed controllers?
(feel free to put a better title on this)
It turns out that you can disambiguate based on the response to isMovingToParentViewController. If it is YES your controller has just been placed topmost on the stack. If it is NO, your controller is returning to topmost, another push on top of it being popped. Example:
-(void)viewWillAppear:(BOOL)animated{
if (self.isMovingToParentViewController == NO) { // returning from even higher controller
[self updateForChangesThatMayHaveHappenedInSubController];
}
[super viewWillAppear:animated];
}
You can use the viewWillAppear: method to update the ui before the view becomes visible. If you want to pass data back up the chain, you should assign yourself as the delegate to your child and call an update function on the delegate before popping.
To have many clients (viewControllers in this case) update their views in response to a change of some shared data, you should use NSNotifications, or you should have the viewControllers observe certain values on the shared data-object (KVO).
ViewController should be as autistic as possible, meaning that they know all about the interface of downstream viewControllers, but have absolutely no idea about what viewController is upstream (talking back to an upstream viewController is usually done through delegation, and only to signal events that might indicate some change in viewController hierarchy, not in shared data state).
Checkout out the stanford lectures by Paul Hegarty, he explains this much better then I can.
I have a common trouble but can't find a right way to solve it, I'll start from beginning.
I'm making a game app with game center and matchmaking. I want to receive and handle invitation from friends. I made singleton class that handle all the Game Center stuff. Right now, I'm in doubt about how to handle invitations (I mean in entire app).
My singleton does handle it and I made additional protocol that calls a delegate method when invitation has been received. So, I decide to implement this protocol in all of my UIViewControllers (I don't know how to do this in a better way, if you know I would very appreciate if you share your experience). But now, I'm stuck with a problem that is in a title. I need to change my UIViewController from current view controller to "game view controller" (it must be shown for the game). I use Navigation View Controller for my app.
So, is there a way to change current view controller to a particular view controller from my "Game Center stuff" singleton class or at least from current ViewController?
For example, I'm in options view controller, that is come from main menu view controller(MainMenuVC→OptionsVC) and I'm receive invitation and I'm accept it. Right now I want to go to "game view controller or GameVC", that is places in another segue path (MainMenuVC→Game ModesVC→GameVC).
I hope my question is clear, if not, I'll provide images to explain, just let me know in the comments below. I think that this question is quite trivial and there are some common practices. Thanks.
Not sure if I understand the issue exactly, but have a look at UINavigationController:setViewControllers: and see if it might be what you are after.
It will allow you to swap one stack of view controllers for another stack. To create your new stack you can use [self.storyboard instantiateViewControllerWithIdentifier: #"whatever"]
Maybe your need is the notification.
add this to OptionVC, after you accept invitation, send this message:
[[NSNotificationCenter defaultCenter] postNotificationName:#"AcceptInvitationNofity" object:self];
and add this to your MainMenuVC:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(receiveEvent:)
name:#"AcceptInvitationNofity"
object:nil];
in receiveEvent function, navigate to GameVC.
I think you have 2 questions from what I've read. Correct me if I'm wrong.
1st question, you're asking if there's a better way to handle a singleton that deals with background stuff such as invites and etc.
So right now, you have a singleton with delegation in every single controller. We don't know if what these delegates do and how specializes they are to your controllers. Since it's in all controllers, I suspect that all controllers have to do mostly the same things once the delegation is called? In that case, you should have a BaseController that implements the delegation methods and have all subsequent controllers be a subclass of that BaseController. Any specialized logic can be overridden in the subclass and [super delegateMethod] can be called.
2nd question is you're asking if you can go from one segue stream to another segue stream within the same UINavigationController. That one is easy. You need to give your Controllers on the storyboard an identifier, instantiate those controllers using the identifier, put it all into an array, then set the navigationcontroller's Controller array to that new array. Here's some code to illustrate that.
-(void)someButtonPressed {
NSMutableArray *viewControllers = [self.navigationController viewControllers].mutableCopy;
[viewController removeLastObject];
[viewController addObject:[self.storyboard instantiateViewControllerWithIdentifier:#"GameModes"]];
[viewController addObject:[self.storyboard instantiateViewControllerWithIdentifier:#"GameVC"]];
[self.navigationController setViewControllers:viewControllers];
}
This will pop your current view controller and make you land on the GameVC controller while preserving the GameModes controllers.
After having received an invitation, you can call this from your current UIViewController:
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"GameVCIdentifier"];
[self.navigationController pushViewController:controller animated:YES];
where "GameVCIdentifier" is the identifier of the UIViewController you want to show. You can set the identifier inside your Storyboard.
Since iOS 6, unwind segues have been available to navigate up the scene hierarchy. I am trying to decide on the cleaner/better/preferred/more maintainable method for passing data to a parent view controller. There are some questions that address this from the technical perspective (e.g. "if I have an unwind do I still need a delegate") but I can't find much that addresses the questions of pros/cons.
Option 1: use a delegate.
Done by passing in the parent view controller as a delegate adhering to a protocol.
Child calls the protocol method to return the data.
If data validation is required by the Parent, return value/dict required to allow child to handle error.
Overhead: Protocol definition and one method in the parent (for data validation and receipt).
Option 2: use an unwind segue
Done by calling the unwind segue from the child.
Child adds a segue on its scene by dragging a button or the storyboard itself to Exit and naming the segue so it can be with performSegueWithIdentifier:sender
Parent implements returnFromSegueName (user named method linked to that segue) to grab the data from the child.
Data validation though can only be implemented by also implementing canPerformUnwindSegueAction:fromViewController:withSender
Data validation failure will require another property on the Child as this method only accepts a BOOL for return value.
Overhead: Two methods, an extra property, plus Storyboard shenanigans.
Overall, delegates are feeling like the cleaner way to go, but perhaps also antiquated. Am I wrong to be leaning in that direction?
I realize now that this isn't truly an answerable question other than to say that neither approach is wrong - they both have their pros and cons. After having tackled both for a week and done more reading on the subject I can at least quantify why you might want to use either an unwind segue or delegates for working between view controllers.
Coupling
Both models are roughly equally (loosely) coupled. Under the hood, an unwind segue is just a delegate where iOS has done the work of wiring it up for you. For delegates, the parent knows about and conforms to the child protocol. For unwind segues, the parent has to be wired to the child on the storyboard for unwind and needs to know the properties of the child to extract the return data. However, if you're new to delegates and just want to get some data out of a child view, unwind segues are probably less intimidating than using protocols with delegates.
Flexibility
Unwind segues are only a good choice if the sole purpose of the child-to-parent interaction is to return data. There does not appear to be a way to cancel an unwind segue in progress. So if the parent has to do any data validation, or if the child needs more than one interaction with the parent, the only way to do this is to have a delegate where multiple methods can be called back to the parent.
Maintainability
If the type or other aspects of the data being returned changes, it will be easier to update the unwind segue as all you have to do is to update the code in your unwind segue to look at the new properties. For the protocol/delegate approach, you will have to update the protocol in the child and the implementation in the parent. However, the simplicity of the unwind segue comes at the cost that you may easily miss places in parent view controllers that require updating because you don't have the compiler checking your contract (the protocol).
The Winner
There isn't one. Which way you go depends on your data needs, comfort level with protocols (they look more intimidating on first glance than they should), complexity of your application, and long term maintenance needs.
For my purposes, I wound up using delegates because my child had to make more than one call back to the parent in some cases. However, in a few instances where I had many pieces of data to pass back, I adopted what I learned from the unwind segue and simply used properties in the child from which the parent could extract the needed information. I also used this as a convenient path for the parent to provide error information to the child. I don't mix and match unwind segues with delegates in the program for consistency with a programming partner, but there's no reason you couldn't do that if you wanted to.
I was very skeptical of storyboards, but I decided to dive in and use them on a new project. I was amazed at the ease with which you can communicate between the two view controllers. When you perform a performSegueWithIdentifier you get a handle to the new ViewController. You can set any exposed properties you want in that new viewController very cleanly and nicely.
Here is an example:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
Student *student = [self.students objectAtIndex:indexPath.row + [self rowAdjuster]];
[[segue destinationViewController] setStudent:student];
}
}
It is very nice and neat. No special protocol that you need to track or maintain.
And Then coming back (I have an IBAction connected to a button in my detail view) You can once again get a nice clean reference to the viewController to which you are returning, and act upon that viewController.
- (IBAction)returnWithStudent:(UIStoryboardSegue *)segue {
UIViewController *vc = [segue sourceViewController];
if ([vc isKindOfClass:[ AddStudentViewController class]]) {
AddStudentViewController *addViewController = (AddStudentViewController *)vc;
if (addViewController.student != nil) {
if ([addViewController hasTakenPhoto]) {
[PhotoHelpers saveImageForStudent:addViewController.student];
}
[StudentController updateStudent:addViewController.student];
}
}
}
Also the segue logic control is nice. One can perform logic checks in shouldPerformSegue which are quite handy.
I've seen lots of junky code that uses protocols of the "send something back to caller" that are really poor at coupling classes. It makes a three-way arrangement-- viewController1 -> protocol -> viewController2, whereas segues make a nice arrangement of viewController1->viewController2.
The segue is a nice way to cleanly and uniquely couple the two classes. I'd strongly recommend it.
I have a view that I want to reuse in different situations. It is a user view that, when touched, will have the viewcontroller push a user detail viewcontroller.
So basically I have a view that can any number of superviews until the viewcontroller. I want that view to be able to notify whatever viewcontroller that is currently being displayed to push the user detail view.
Is there a way besides using NSNotificationCenter to do this? Is NSNotificationCenter my best option? I've tried to put in a protocol/delegate, but that isn't working out for me.
Thanks!
------------------------Response to a comment----------------
I would like to have it so it is dynamic. That is partially my problem. I will use this view throughout my code and when I make updates/changes, I don't want to have to change the actual user view to make things work
An example would be adding this user view on the following hierarchy: viewcontroller->tableview->tableviewcell->userview. But then I'd also like to add it like this: viewcontroller->userview.
navigationController.topViewController may be helpful in this case. Or if your app is using a single navigation stack, you could handle this notification in the appDelegate
#interface AppDelegate
#property (nonatomic, strong) UINavigationController *nav;
...
[nav pushViewController:userVC animated:YES];
I think it does make sense to use an NSNotification in this case. Per MVC, the UIView handling the touch event should not need to know much about the View Controller hierarchy it lives in. Notifications handle that issue.
I am thinking that I will subclass a UINavigationController and register for my NSNotification there, then i won't have to worry about registering on each UIViewController in my app. I'll leave this answer here for a bit without checking it as the answer to see what kind of side effects this might have.