I have a UITabBarController with four tabs. In each of the view controllers presented when a tab is selected I have a reset button. Tapping the button will change the appearance of all the view controllers. In particular, it will change the text of some labels in the different view controllers.
Is there some recommended way to update all the view controllers of a UITabBarController at the same time i.e. to make them reload their views?
My current approach is to make those view controllers conform to a protocol
#protocol XYReloadableViewController
- (void)reloadContents;
#end
and then send the message -reloadContents to all the view controllers when the button is tapped:
- (IBAction)touchUpInsideResetButton {
// ...
NSArray *viewControllers = self.tabBarController.viewControllers;
for (UIViewController<XYReloadableViewController> *viewController in viewControllers) {
[viewController reloadContents];
}
}
Then in each of the view controllers I would have to implement that method:
- (void)reloadContents {
[self.tableView reloadData];
// other updates to UI ...
}
But that seems a little too complicated. So is there an easier way to tell the view controllers to reload their views?
Edit: And what happens if I present a UINavigationController in some of the tabs or a container view controller? I would need to pass the message along the chain of all its child view controllers...
You can create ReloadViewController and all you contrlollers inheritance
from him.
ReloadViewController have property UIButton and methods:
-(void)reloadContents;
-(IBAction)touchUpInsideResetButton:(id)sender;
in .m file:
-(void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reloadContents)
name:#"MyNotification"
object:nil];
}
- (IBAction)touchUpInsideResetButton:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyNotification"
object:nil];
}
in your viewControllers need only override method reloadContents
Notifications sound like a better fit for this. When view controllers need to be reset, broadcast an NSNotification and have any view controllers that might need to reset themselves listen for that notification, and trigger what they need to do. That way it doesn't matter how far down a navigation stack they are.
You might want to defer updates until the view actually appears. You could set a BOOL needsUpdate when the VCs receive the notification, but only do the actual update in viewWillAppear:, to save resources and prevent a large number of updates from going off at once (and perhaps blocking the main thread).
If this behaviour is common to all your view controllers, make a UIViewController subclass to prevent repeating code and have them all inherit from that. Alternatively, (if you're using Apple VC subclasses) make a category on UIViewController to add the notification methods.
Related
I have a UIView which is added to UIViewController.
UIView is a separate class and separate .h and .m files.
I want to present another UIViewController or something. How to access the UIViewController that added this UIView from UIView ?
Short answer: No, you can't know which exactly is the view's view controller, because that would break MVC principles.
I don't think you understood what MVC means and stands for. It wouldn't be a good approach to present a view controller from a view object of another view controller. It is the view controller who should provide any information view needs from the outside world.
UIView objects are meant to just display UI components to screen and are responsible for drawing and laying out their child views correctly.
As I said above, you should handle those kind of interactions between the views (or communication channels, whatever you call it) always in controllers to where they actually belong. In this context, you should present any view controller from another view controller, not another view. If you need to send messages from a view to its view controller, you can make use of the delegate approach or NSNotificationCenter class.
If it were up to me, I would personally prefer using delegate when view needs some information from its view controller. It is more understandable than using notification center as it makes it much easier to keep track of information flow. However in your case, in other words where view controller needs information from view (reverse communication), I'd go with the notification center.
So let's enrich this conversation with an example:
#implementation SomeView
- (IBAction)buttonClicked:(id)sender
{
NSDictionary *userInfo = #{#"value": [self someCalculatedValue]};
[[NSNotificationCenter defaultCenter] postNotificationName:ViewButtonClickedNotification object:self userInfo:userInfo];
}
#end
Note that, you should never leave the object: argument of [NSNotificationCenter:postNotificationName:object] method as nil since it will help the controller distinguish the notification sent by its view from other notifications.
#implementation SomeViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(viewButtonClicked:) name:ViewButtonClickedNotification object:self.view];
}
- (void)viewButtonClicked:(NSNotification *)
{
NSNumber *someCalculatedValue = notification.userInfo[#"value"];
[self presentViewController:[[UIViewController alloc] initWithCalculatedValue:someCalculatedValue] animated:YES completion:nil];
}
#end
For more information about communication patterns in iOS, you might want to take a look at this great article in order to comprehend how they work.
I have a controller for a "settings view" in where the user changes some of the properties of what's drawn in my MyView view. But, I can't figure out how to call setNeedsDisplay on MyView instead of my SettingsView.
MyView is an UIView, and it has its own controller called MyViewController.
Inside this view, I'm drawing rectangles of a kind, and with different colors. In SettingsViewController I change a parameter which affects what kind of rectangles are drawn in MyView. However, for that to happen, the user has to (they're on different tabs) switch views, and perform some action so that setNeedsDisplay gets called. I'm trying to call setNeedsDisplay FROM SettingsViewController, but I can't do self.view setNeedsDisplay beacause it'll call setNeedsDisplay on MyView. Does this make more sense?
You just need to call setNeedsDisplay on the view that you want to redraw. So if you're in SettingsViewController you need a handle to MyViewController. You mentioned you're in a TabBarController, so you could access the view through this. Eg (assuming MyViewController is the first tab and you're in SettingsViewController in another tab):
[[self.tabBarController.viewControllers[0] view] setNeedsDisplay]
A better question to ask would be "How can I design this so that my view controllers are self-contained and not dependent on the hierarchy of other view controllers?"
It seems like the best solution in your case might be to use notifications to let your drawing view controller know that it needs to refresh because settings have changed.
In your settings view controller.h add:
#define kSettingsChangedNotification #"SettingsChangedNotification"
In your Settings view controller, when settings have been updated you can use:
[[NSNotificationCenter defaultCenter] postNotificationName:kSettingsChangedNotification
object:self
userInfo:nil];
And then in your drawing view controller add the following to your viewDidLoad:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(onSettingsChanged:)
name:kSettingsChangedNotification
object:nil];
And, finally define onSettingsChanged in your drawing view controller:
-(void)onSettingsChanged:(NSNotification*)notification
{
[self.view setNeedsDisplay];
}
I'm facing a strange problem..
I want to add a UIViewcontroller (called iView) to my current View.
I do it by calling
iView = [[KFKaraokeInfosView alloc] initWithKaraoke:karaoke NibName:#"InfosView" commingFromPlayer:NO];
iView.songTitle.text = karaoke.title;
[self.view addSubview:iView.view];
In the viewDidLoad of iView, I add it as an observer to the NotificationCenter for a certain notification, like this
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = #"About";
if ([karaoke.styles count] == 0)
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"GetInfosOfSong" object:self.karaoke];
}
else
{
shouldSetup = YES;
}
[self.navigationController setNavigationBarHidden:NO animated:YES];
[self.navigationController.navigationBar setBarStyle:UIBarStyleBlack];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(setup) name:GetSongInfos object:nil];
[optionsTableView setBackgroundView:nil];
}
The problem is when I call autorelease method on iView at the initialisation, the notification is never catched (so setup method is never called), but if I don't call autorelease for iView, it works.
I don't understand the memory management in this situation, can someone help me to understand ?
The container view controller methods are found in Implementing a Container View Controller of the UIViewController Class Reference, and sample code can be found in Creating Custom Container View Controllers of the View Controller Programming Guide.
Thus, in iOS 5 and later, you should probably have:
iView = [[[KFKaraokeInfosView alloc] initWithKaraoke:karaoke NibName:#"InfosView" commingFromPlayer:NO] autorelease];
[self addChildViewController:iView];
iView.songTitle.text = karaoke.title;
[self.view addSubview:iView.view];
[iView didMoveToParentViewController:self];
If this is iOS 4 or earlier, it doesn't support proper containment. You can manually kludge it, by adding the view like you are, with no autorelease:
iView = [[KFKaraokeInfosView alloc] initWithKaraoke:karaoke NibName:#"InfosView" commingFromPlayer:NO];
iView.songTitle.text = karaoke.title;
[self.view addSubview:iView.view];
You'd obviously keep a copy of the child view controller in some ivar of the parent view controller, not autorelease it, but rather explicitly release the child's controller when the child's view is dismissed. Note, since you're operating in a pre-containment iOS4 world, your child controller is not guaranteed of receiving various events (notoriously rotation events). But you should receive your notification center events.
An alternative to this ugliness of trying to fake containment in iOS 4 is to not use a child view controller at all, but just add the child view to the parent view. You can actually add it to the parent controller's NIB, but just hide it. Then, when you want to present it, unhide it. But keep everything in the parent view controller and it's NIB. The method that I described above, faking containment, might work (I've seen people do it), but it gives me the heebie jeebies. This is simpler.
I have a viewController I've built in storyboard. I also have a NSObject Subclass which acts as my model, which sends and listens for API requests and responses. When a method fires in my model, I want to present a modal View of my viewController from whatever view happens to be visible at the time.
An example would be if my API hears "show this view" I want to show viewController regardless of what view is being shown.
Conceptually, how does one do this?
EDIT: I don't know which view controller will be showing when I want to present my modal viewController. Also, I need to pass params from my model to the modalVC when it's presented.
I would send a notification from the model telling "someone" that some view needs be displayed.
NSDictionary *userInfo = #{ #"TheViewKey": viewToDisplay];
[[NSNoticationCenter defaultCenter] postNotificationName:#"NotificationThatThisViewNeedsToBeDisplayed" object:self userInfo:userInfo];
And then on the delegate (or the active view controller) would register to this notification and handle the display.
// self is the delegate and/or the view controller that will receive the notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleViewToDisplay:) name:#"NotificationThatThisViewNeedsToBeDisplayed" object:nil];
If you put in the view controller remember to remove self from the observers when the view is not visible:
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"NotificationThatThisViewNeedsToBeDisplayed"];
This way your model is decoupled from the presentation.
You have the current viewController (any viewController subclass) present the new view using:
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
EDIT: To find the top view controller, you ask the UITabBarController for the selectedViewController (if you use a tabBarController) to get the 'seed', or start with the window.rootViewController.
Once you are past any tabBarControllers, then you should only have UIViewController subclasses and UINavigationControllers. You can use a loop like this:
- (UIViewController *)frontmostController:(UIViewController *)seed
{
UIViewController *ret;
if([seed isKindOfClass:[UINavigationController class]]) {
ret = [(UINavigationController *)seed topViewController];
} else
if([seed isKindOfClass:[UIViewController class]]) {
ret = seed.presentedViewController;
}
return ret ? [self frontmostController:ret] : seed;
}
The root level view controller in my iPad application is a UISplitViewController. Thus, it has 2 view controllers :
one master view controller (item 0 of the viewControllers property)
one detail view controller (item 1 of the viewControllers property)
The detail view controller is a custom view controller that I change depending on taps and events in my master view controller.
To change this detail view controller, I use the following code:
- (void)replaceSecondViewControllerBy:(UIViewController *)viewController {
[[self.viewControllers objectAtIndex:1] dismissModalViewControllerAnimated:NO];
NSArray *newVC = [NSArray arrayWithObjects:[self.viewControllers objectAtIndex:0], viewController, nil];
self.viewControllers = newVC;
}
My problem is that when my app receives a memory warning event, the didReceiveMemoryWarning method is called for all my view controllers, except for former detail view controllers. And they're not being deallocated because they still are delegates for other objects (including asynchronous methods that might still be running).
My questions are :
What are the rules for a UIViewController to receive a didReceiveMemoryWarning message ? Why don't my former detail view controllers receive them ?
Can I safely call didReceiveMemoryWarning or viewDidUnload myself on these old view controllers ?
It looks like a UIViewController subscribes to UIApplicationDidReceiveMemoryWarningNotification when it is created. It removes observing the notification when it is deallocated. So didReceiveMemoryWarning is called even if the the controller's view is not in the view hierarchy. So make sure your controllers are not deallocated.
It is unlikely but if you happen to be using the following code to remove notification observation from your controllers they will also stop listening memory notifications.
[[NSNotificationCenter defaultCenter] removeObserver:controller name:nil object:nil];