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.
Related
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.
This is puzzling me.
The context
The original tutorial I'm following.
Where the segue is added to the Main View via a custom segue:
- (void) perform {
MainViewController *source = (MainViewController *)self.sourceViewController;
UIViewController *destination = (UIViewController *) self.destinationViewController;
for(UIView *view in source.main.subviews){
[view removeFromSuperview];
}
source.currentViewController = destination;
destination.view.frame = CGRectMake(0, 0, source.main.frame.size.width, source.main.frame.size.height);
[source.main addSubview:destination.view];
}
The TextField is connected as delegate in the child View Controller. All things being equal I get the app crashed without any message.
The workaround
In the Main View Controller, in -(void)prepareForSegue: I've added [segue.destinationViewController setDelegate:self]; in the meantime I've added a property in the child View Controller id<UITextFieldDelegate> delegate and modified the textfield delegate as self.delegate.
This works, but the trouble is that I've to set the delegated methods in Main View Controller which is not quite efficient as I have more View Controllers to add.
The Objective
How do I set each View Controller to be the delegate for itself without crashing?
The immediate cause of your error is that the view controller that your views belong to is being deallocated. The fact that your views are on screen while their view controller is deallocated highlights a fundamental flaw in the approach of taking views off one view controller and adding them to another. View controller containment is the correct way to solve an issue like this.
Changing the currentViewController property to strong will fix the memory management issue you're seeing, but it's just a bandaid. Your currentViewController will still be missing rotation methods, appearance and disappearance methods, layout methods, and so forth. View controller containment ensures these methods get called for the view controller whose views are on screen.
Here is an altered version of your project that illustrates how to use view controller containment. I think that will be a better solution than manually removing and adding subviews of the view controllers themselves. See the Apple docs for more info on custom view controller containers.
At first, let's see crash report. Please, do the following:
1. Add Exception Breakpoint
2. Edit it as in the picture
You should create a custom class for the destinationViewController wich will implement UITextFieldDelegate
#interface DestinationViewController <UITextFieldDelegate>
#end
And from storyboard add the class to UIViewController that has TextField
And make the connections for elements and TextField delegate.
Implement delegate methods.
You will not need the implementation of prepareForSegue: anymore. You will have two different classes with different elements. Only if you need to pass something from source to destination then you use prepareForSegue:
Hope you'll understand
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.
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.