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.
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 am implementing MDSpreadView, a third party controller, in one of my projects. I have simply included every file related to it including xib. Calling it as subview.
The hierarchy of calling views is like this: there is a uiviewcontroller in which I am adding UIView as subview, and from that subview I am calling uiviewcontroller as subview.
MDViewController *MDvc = [[MDViewController alloc]initWithNibName:#"MDViewController_iPhone" bundle:nil ];
[self addSubview:MDvc.view];
It Appears fine but when I touch to scroll or select or for anything, Thread 1:EXC_BAD_ACCESS error occurs at selection delegate. Whereas delegates are implemented as it is in demo proj.
here is the image
I know there is some issue in calling subviews. How do I solve this?
The idea of taking a view out of one controller and inserting it as a subview in a different controller is a common cause of crashes. If you have to do it, make sure that the original controller (MDViewController in this case) is not released. You can do that by making it a strong property of the object that is hijacking its view or, better, look at the documentation for how to implement a container controller.
Finally get to know how to handle multiple views, especially when you have subviews and viewcontroller. Solution is very simple , in such situations you need to have delegates or there is a great thing which apple gives itself is NSNotification. I did solve my problem through NSNotification. On the button press(from where i have to call other view) i post a notification like this:
[[NSNotificationCenter defaultCenter] postNotificationName:#"WhatEverYouWantTocallIt" object:nil];
and i added observer in the class which i needed to call, like this:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(bringSpreadViewToFront) name:#"WhatEverYouWantTocallIt" object:nil];
AND added a selector, in selector u should handle it according to scenario you have i did this:
-(void)bringSpreadViewToFront{
NSArray *viewsToRemove = [self.view subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
MDViewController *md = [[MDViewController alloc] initWithNibName:#"MDViewController_iPhone" bundle:nil];
[self presentViewController:md animated:YES completion:nil];
}
i first removed the subviews one by one and then present my view controller, presenting pushing its your choice. it works perfect.. Cheers :)
Thank you Phillip for pushing me in a direction close to the solution.
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.
Suppose I have a container controller that accepts an array of UIViewControllers and lays them out so the user can swipe left and right to transition between them. This container controller is wrapped inside a navigation controller and is made the root view controller of the application's main window.
Each child controller makes a request to the API and loads a list of items that are displayed in a table view. Based on the items that are displayed a button may be added to the navigation bar that allows the user to act on all the items in the table view.
Because UINavigationController only uses the UINavigationItems of its child view controllers, the container controller needs to update its UINavigationItem to be in sync with the UINavigationItem of its children.
There appear to be two scenarios that the container controller needs to handle:
The selected view controller of the container controller changes and therefore the UINavigationItem of the container controller should update itself to mimic the UINavigationItem of the selected view controller.
A child controller updates its UINavigationItem and the container controller must be made aware of the change and update its UINavigationItem to match.
The best solutions I've come up with are:
In the setSelectedViewController: method query the navigation item of the selected view controller and update the leftBarButtonItems, rightBarButtonItems and title properties of the container controller's UINavigationItem to be the same as the selected view controller's UINavigationItem.
In the setSelectedViewController method KVO onto the leftBarButtonItems, rightBarButtonItems and title property of the selected view controller's UINavigationItem and whenever one of those properties changes up the container controller's UINavigationItem.
This is a recurring problem with many of the container controllers that I have written and I can't seem to find any documented solutions to these problems.
What are some solutions people have found to this problem?
So the solution that I have currently implemented is to create a category on UIViewController with methods that allow you to set the right bar buttons of that controller's navigation item and then that controller posts a notification letting anyone who cares know that the right bar button items have been changed.
In my container controller I listen for this notification from the currently selected view controller and update the container controller's navigation item accordingly.
In my scenario the container controller overrides the method in the category so that it can keep a local copy of the right bar button items that have been assigned to it and if any notifications are raised it concatenates its right bar button items with its child's and then sends up a notification just incase it is also inside a container controller.
Here is the code that I am using.
UIViewController+ContainerNavigationItem.h
#import <UIKit/UIKit.h>
extern NSString *const UIViewControllerRightBarButtonItemsChangedNotification;
#interface UIViewController (ContainerNavigationItem)
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems;
- (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem;
#end
UIViewController+ContainerNavigationItem.m
#import "UIViewController+ContainerNavigationItem.h"
NSString *const UIViewControllerRightBarButtonItemsChangedNotification = #"UIViewControllerRightBarButtonItemsChangedNotification";
#implementation UIViewController (ContainerNavigationItem)
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems
{
[[self navigationItem] setRightBarButtonItems:rightBarButtonItems];
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:UIViewControllerRightBarButtonItemsChangedNotification object:self];
}
- (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem
{
if(rightBarButtonItem != nil)
[self setRightBarButtonItems:#[ rightBarButtonItem ]];
else
[self setRightBarButtonItems:nil];
}
#end
ContainerController.m
- (void)setRightBarButtonItems:(NSArray *)rightBarButtonItems
{
_rightBarButtonItems = rightBarButtonItems;
[super setRightBarButtonItems:_rightBarButtonItems];
}
- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
if(_selectedViewController != selectedViewController)
{
if(_selectedViewController != nil)
{
// Stop listening for right bar button item changed notification on the view controller.
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter removeObserver:self name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController];
}
_selectedViewController = selectedViewController;
if(_selectedViewController != nil)
{
// Listen for right bar button item changed notification on the view controller.
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(_childRightBarButtonItemsChanged) name:UIViewControllerRightBarButtonItemsChangedNotification object:_selectedViewController];
}
}
}
- (void)_childRightBarButtonItemsChanged
{
NSArray *childRightBarButtonItems = [[_selectedViewController navigationItem] rightBarButtonItems];
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray:_rightBarButtonItems];
[rightBarButtonItems addObjectsFromArray:childRightBarButtonItems];
[super setRightBarButtonItems:rightBarButtonItems];
}
I know this question is old, but I think that I found the solution for this problem!
The navigationItem property of a UIViewController is defined in a category/extension in the UINavigationController header file.
This property is defined as:
open var navigationItem: UINavigationItem { get }
So, as I just found out, you can override the property in the container view controller, in my case:
public override var navigationItem: UINavigationItem {
return child?.navigationItem ?? super.navigationItem
}
I tried this approach and it's working for me. All buttons, title and views are being shown and updated as they change on the contained view controller.
The accepted answer works, but it breaks the contract on UIViewController, your child controllers are now tightly coupled with your custom category and must use its alternative methods in order to work correctly...
I had this issue using the RBStoryboardLink container, and also on a custom tab bar controller of my own, so it was important it would be encapsulated outside of a given container class, so I created a class that has a mirrorVC property (usually set to the container, the one who will listen for notifications) and a few register / unregister methods (for navigationItems, toolbarItems, tabBarItems, as your needs see fit).
For example when registering/unregistering for toolbarItems :
static void *myContext = &myContext;
-(void)registerForToolbarItems:(UIViewController*)viewController {
[viewController addObserver:self forKeyPath:#"toolbarItems" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:myContext];
}
-(void)unregisterForToolbarItems:(UIViewController*)viewController {
[viewController removeObserver:self forKeyPath:#"toolbarItems" context:myContext];
}
The observe action will handle receiving the new values and forwarding them to the mirrorVC:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if(context == myContext) {
id newKey = [change objectForKey:NSKeyValueChangeNewKey];
id oldKey = [change objectForKey:NSKeyValueChangeOldKey];
//no need to mirror if the value is the same
if ([newKey isEqual:oldKey]) return;
//nil values comes packaged in NSNull
if (newKey == [NSNull null]) newKey = nil;
//handle each of the possibly registered mirrored properties...
if ([keyPath isEqualToString:#"navigationItem.leftBarButtonItem"]) {
self.mirrorVC.navigationItem.leftBarButtonItem = newKey;
}
//...
//as many more properties as you need forwarded...
else if ([keyPath isEqualToString:#"toolbarItems"]) {
[self.mirrorVC setToolbarItems:newKey animated:YES];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Then in your container, at the right moments, you register and unregister
[_selectedViewController unregister...]
_selectedViewController = selectedViewController;
[_selectedViewController register...]
You must be aware of a potential pitfall though: not all desirable properties are KVO compliant, and the ones that do aren't documented to be - so they can stop being or misbehave at any time.
The toolbarItems property, for example, is not. I created a UIViewController category based on this gist ( https://gist.github.com/brentdax/5938102 ) that enables KVO notifications for it so it works in this scenario. Note: the gist above wasn't necessary for UINavigationItem, iOS 5~7 sends out proper KVO notifications for it, with that category I would get double notifications for UINavigationItems. It worked flawlessly for toolbarItems!
Have you considered NOT wrapping your container view controller in a UINavigationController and just adding a UINavigationBar to your view? Then you can push your child view controller's navigation items directly to that navigation bar. Essentially your container view controller would replace a normal UIViewController.
I know this is an old thread, but I just ran into this issue and thought someone else might as well.
So for future reference, I did it as follows: I sent a block to the child view controller, which just sets the parent's UINavigationItem's right button. Then I created a UIBarButtonItem as normal in the child view controller, calling some method in that same controller.
So, in ChildViewController.h:
// Declare block property
#property (nonatomic, copy) void (^setRightBarButtonBlock)(UIBarButtonItem*);
And in ChildViewController.m:
self.myBarButton = [[UIBarButtonItem alloc]
initWithTitle:#"My Title"
style:UIBarButtonItemStylePlain
target:self
action:#selector(didPressMyBarButton:)];
...
// Show bar button in navigation bar
// As normal, just call it with 'nil' to hide the button
if (self.setRightBarButtonBlock) {
self.setRightBarButtonBlock(self.myBarButton);
}
...
- (void)didPressMyBarButton:(UIBarButtonItem *)sender {
// Do something here
}
And finally in ParentViewController.m
// Initialise child view controller
ChildViewController *child = [[ChildViewController alloc] init];
// Give it block for changing bar button item
__weak typeof(self) weakSelf = self;
child.setRightBarButtonBlock = ^void(UIBarButtonItem *barButtonItem) {
[weakSelf.navigationItem setRightBarButtonItem:barButtonItem animated:YES];
};
// Finish the parent-child VC dance
That's it. This feels good to me because it keeps the logic pertaining to the UIBarButtonItem in the view controller which is actually interested in it.
Note: I should mention that I am not a pro. This may just be a terrible way to do it. But it seems to work just fine.
I am using my app delegate to transition between view controllers. When the delegate decides it no longer needs the view controller, based on messages from the server, it needs to remove the current view and replace it with another one. Currently my code looks like the following:
- (void) showFight: (NSNotification*) notification
{
if(self.window.rootViewController != self.fightViewController)
{
NSDictionary* dict = [notification userInfo];
FightViewController *fightView = [[FightViewController alloc]
initWithNibName:#"FightViewController" bundle:nil];
fightView.userId = _userId;
self.fightViewController = fightView;
[fightView release];
[self.radarViewController.view removeFromSuperview]; // Doesn't work.
self.window.rootViewController = self.fightViewController;
[self.fightViewController showMonster:dict];
}
}
I know my view controller isn't being removed because I can hear sound effects from it in the background.
I want to completely destroy the view controller, as I only want one view controller in memory at any time. I plan to create the view controller each time from scratch, as shown in the code above. Am I doing this improperly?
The problem here seems to be that you are not releasing the view controller. Think about what actually happens in your code at:
[self.radarViewController.view removeFromSuperview];
You remove the view from its super view. The view controller still exists, and what it does is control what should be shown on the view, and in your case apparently playing sound.
Put in an easy way: The view controller is an object. It has a child, the view. That's another object. You remove the view from another view, but the object controlling the removed view still lives (and actually, so does the view object).
If you want to kill the entire view controller, call this after removing the view from its superview:
[self.radarViewController release];
Or, if the view is a retain property (which i assume by looking at your code) you can also use:
self.radarViewContoller = nil;
which automatically releases for you in the synthesized setter.
When the view controller is released, its reference count is subtracted by one. If the reference count reaches zero, then the controller will be deallocated.
So far I understand your problem is to add the new ViewController on server notify and change the current view with new View. First of all you've to add the view controller just like below because the reference won't help to update the view.
[self.window.rootViewController.view addSubview: self.fightViewController.view]
In my opinion you need to tag your Controllers and check before adding the controller that if it's already exist in the memory, otherwise the pool of object will leak. Just Say No to Memory Leaks!!
- (void) showFight: (NSNotification*) notification
{
UIView *fightView = (UIView *)[self.window.rootViewController.view viewWithTag: FIGHT_VIEW_TAG];
if (self.window.rootViewController.view.tag != fightView.tag) {
NSDictionary* dict = [notification userInfo];
FightViewController *fightView = [[FightViewController alloc]
initWithNibName:#"FightViewController" bundle:[NSBundle mainBundle]];
//Remove the current view.
[self.window.rootViewController.view removeFromSuperview]; // If you're adding the fighting View in the root View, then why are you trying to remove current view through radar controller which has already added in the window (root view).
fightView.userId = _userId;
[fightView setTag: FIGHT_VIEW_TAG];
[self.window.rootViewController.view addSubView: self.fightViewController.view];
[self.fightViewController showMonster:dict];
[fightView release];
}
}
You don't need to take them as global until your requirements are different.