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];
}
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.
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 am trying to get a popup effect and want to design the popup view in another view controller so i can use the xib to do it.
When i used the presentViewController or pushViewController and set the background to transparent, i end up seeing the Window's background color.
I tried this code to add subview to the navigation controller's view so that i can have the Info view cover the entire screen with a transparent background. I also have tab bar to cover up as well.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
My problem is inside my InfoVC when i try to dismiss it, the app will crash with some EXC_BAD_ACCESS message:
[self.view removeFromSuperview];
EDIT:
I found a way to stop it crashing but setting the InfoVC as a property in the MainVC. I think the reason for crash is when i call "self.view" in the action inside the InfoVC, it doesn't know that self is the InfoVC inside MainVC.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
No no no no. Never never do that.
There is an elaborate dance that you must traverse in order to put a view controller's view inside another view controller's view (or remove it afterwards) if it doesn't come with built-in facilities for doing this (the way a UISplitViewController does, or the way a navigation controller manages the views of the view controllers that are pushed and popped within it).
Read up on customer container controllers. One of the examples from my book is here:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch19p556containerController/p476containerController/ViewController.m
Shouldn't you be using the following to remove the view from its superview?
[vc.view removeFromSuperview];
You can never have a UIView remove it's subviews, the subviews themselves must remove themselves from it's superview. You can easily loop through subviews and have them removed like so
for (UIView *view in vc.view.subviews) {
[view removeFromSuperview];
}
Docs for reference:
http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
After a "modally" presented view controller has appeared the views under the now presented view controller will be removed; this saves memory, and eases rendering. In your case, though, you also end up seeing the window behind the "modally" presented view.
The natural, and seemingly logical, next step is to simply take one view controller's view and cram it into another. However, as you have discovered, this is problematic. With the newly inserted view safely retained by the view hierarchy it is safe, but the new view controller is not so lucky, it is quickly deallocated. So when this new view tries to contact its controller you will get an EXC_BAD_ACCESS and crash. One workaround, again as you have found, is to simply have the original view controller keep a strong reference to the new view controller. And this can work... badly. There's still a good chance you will get an UIViewControllerHierarchyInconsistencyException.
Of course if you simply want to add a small view you create in IB you don't need to use a view controller as the "File's Owner" and there are many examples of creating an instance of a view from a xib file.
The more interesting question here is, "How would/does apple do it?" Apple consistently says that a view controller is the correct controller for an encapsulated unit of work. For example, their TWTweetComposeViewController, you present it, and it seems to float. How?
The first way of accomplishing this that comes to my mind is to have a clear background that isn't clear. That is, create an image of the screen before the presented view controller appears and set that as the background before the presenting view is removed. So for example(Explanation to follow):
QuickSheetViewController.xib
QuickSheetViewController.h
#import <UIKit/UIKit.h>
#interface QuickSheetViewController : UIViewController
- (IBAction)dismissButtonPressed:(id)sender;
#end
QuickSheetViewController.m
#import "QuickSheetViewController.h"
#import <QuartzCore/QuartzCore.h>
#implementation QuickSheetViewController {
UIImage *_backgroundImage;
}
-(void)renderAndSaveBackgroundImageFromVC:(UIViewController *)vc{
UIGraphicsBeginImageContext(vc.view.bounds.size);
[vc.view.layer renderInContext:UIGraphicsGetCurrentContext()];
_backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// save an image of the current view, and set our background to clear so we can see the slide-in.
[self renderAndSaveBackgroundImageFromVC:self.presentingViewController];
self.view.backgroundColor = [UIColor clearColor];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Time to use our saved background image.
self.view.backgroundColor = [UIColor colorWithPatternImage:_backgroundImage];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
// Set our background to clear so we can see the slide-out.
self.view.backgroundColor = [UIColor clearColor];
}
- (IBAction)dismissButtonPressed:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
The majority of this example hinges upon the renderAndSaveBackgroundImageFromVC: method. In which, we create a graphics context render the view we are about to cover into it, and then create a UIImage to later (in viewDidAppear) use as a background.
Now simply use it like:
QuickSheetViewController *newVC = [[QuickSheetViewController alloc] initWithNibName:nil bundle:nil];
[self presentViewController:newVC animated:YES completion:nil];
You will see through the background just long enough for the animation to happen, then we use our saved image to hide the removal of the presenting view.