In IB I created UIView inside of a UIScrollView. File's owner for both is a UIViewController named JLViewController. The UIView is wired to class BodyClock.
The BodyClock class draws a graph inside the view. It also creates several small views which act as touch hot spots. When a hot spot is touched it displays an informative alert with a button for more detail. I need to tell JLViewController to display the detailed information. I thought I could do this by making the ViewController the HotSpot's delegate. Being as how I am creating the hot spots in the BodyClock class, I can't figure out how to set the hot spot delegate to JLViewController. I am trying to do something like this..
//Code in BodyClock
//create the hot spot
id viewController = [self nextResponder];
HelpHotSpot *helpHotSpot = [[HelpHotSpot alloc] initWithFrame:CGRectMake(start_x, melatoninHeightEnd_y, 80, 40)];
helpHotSpot.delegate = viewController;
[viewController addSubview:helpHotSpot];
[helpHotSpot release];
//Code in the HotSpot after touch and request for more info
//notify JLViewController to display the details
if ([self.delegate respondsToSelector:#selector(hotSpotMore:)]) {
[self.delegate hotSpotMore:itemDetails];
}
Everything works except that respondsToSelector fails. If I NSLog viewController or self.delegate I get...
UIScrollView: 0x7443c20; frame etc...
I was expecting JLViewController: instead of UIScrollView: so I think this is the problem.
How do I set the delegate to the ViewController for these subviews?
Is it possible, or should I be using notification instead?
This:
id viewController = [self nextResponder];
isn't doing what you think it is doing. This doesn't get a view's controller but instead looks at the chain of responders and determines which is the next one for responding to events - these are typically views or controls (not to be confused with controllers). It appears that after your hot spot view, the scoll view is next.
You'll have to get the pointer to the controller correct. Maybe in your hot spot view you could add:
IBOutlet JLViewController *viewController;
And in interface builder connect this to your view controller.
Related
I am pretty new to iPhone programming, and was playing around with an app yesterday trying different scenarios with view controllers and nib files. So, I started a new app with a FirstViewController (FVC for short) and an FVC.xib.
I layed out a quick view in FVC.xib and ran the app - view displays, great.
I now wanted to have a second view I could add on top of the main view. So I went ahead and created SecondViewController.xib (SVC) but did not create the .m and .h files. I went about trying to load both these views from the same view controller, and here is where my question lies:
I created a button in FVC.xib and created an IBAction like this:
- (IBAction)loadSVC {
FirstViewController *viewController = [[FirstViewController alloc] initWithNibName:#"SecondViewController" bundle:[NSBundle mainBundle]];
secondView = viewcontroller.view;
[viewController release];
[self.view addSubView:secondView];
}
So this works great and adds the contents of SVC.xib, but when I try and remove that view from the superview, the app crashes:
[secondView removeFromSuperview];
If I actually create a view controller for SVC, use that to instantiate my view in FVC, and move the remove code to the SVC:
[self.view removeFromSuperview];
Everything works. My question - I kind of get why my first method crashes, but I was hoping someone could explain why and what goes on behind the scenes. I'm still a noob with object oriented programming, so what is actually happening in my first case where I create a new instance of FirstViewController and add its view to self.view? Why can't I release it (I assume because the original view is associated with FirstViewController, and when I create a new instance with the second xib it messes everything up) - I'd love a more technical explanation as to what is happening...
Thanks much!!
EDIT to add more info in response to Nick's reply below
Nick - so your answer did clear my thinking a bit in regards to the retain count, etc... I did another test app trying to get this working from a single view controller - think, for example, that I wanted to display an Alert or Welcome message to the user (I know in a real app there are different methods to accomplish this, but this is more of a learning experience) -- so I have my main view # MainViewController and layout my alert message in a xib called alert.xib -- so there is no logic behind the alert message, no reason for it to have a view controller that I can see, my end goal being loading/unloading this on top of my main view from the main view's view controller (or understanding why it is impossible)
I tried this using instance variables as you recommended:
In MainViewController.h:
#import <UIKit/UIKit.h>
UIViewController *secondController;
UIView *secondView;
#interface MainViewController : UIViewController {
}
#property(nonatomic, retain) UIViewController *secondController;
#property(nonatomic, retain) UIView *secondView;
- (IBAction)loadSecond;
- (IBAction)removeSecond;
#end
In MainViewController.m:
#import "MainViewController.h"
#implementation MainViewController
#synthesize secondController, secondView;
- (IBAction)loadSecond {
secondController = [[MainViewController alloc] initWithNibName:#"alert" bundle:[NSBundle mainBundle]];
secondView = secondController.view;
[self.view addSubview:secondView];
}
- (IBAction)removeSecond {
//I've tried a number of things here, like [secondView removeFromSuperview];, [self.secondView removeFromSuperview];, [secondController.view removeFromSuperview];
}
- (void)dealloc {
[secondController release];
[secondView release];
[super dealloc];
}
So - this works to load the alert view, but the removeSecond button does nothing (I did use NSLog to verify the removeSecond method is fired) - why?
Second, and most importantly - is this even possible, or is it horrible practice? Should every nib/view I am manipulating have their own view controller? Am I wrong to think I could just make a new instance of MainViewController and use it to display and remove this no-functionality, very temporary view? (And yes, I realize I could easily create this view programatically or accomplish the end goal in many different ways which would be easier, but I'm trying to really learn this stuff and I think figuring this out will help...
Thanks for the help!
You created a view controller
You accessed its view which caused controller to create the view and call the delegates (i.e. viewDidLoad)
Controller returns the view that you asked for
Now you add the view as a subview which increases its retain count
Controller is released and it releases the view, BUT since view's retain count was increased the view is still there
You try to remove the view, it is unloaded and delegates are to be called (e.g. viewDidUnload), however that messes up since the controller who created the view is released and that piece of memory is... smth else :)
That's why the first method doesn't work.
The second method is NOT correct either but it works because:
You remove controller's view from superview but since controller itself is not released (you didn't call [self release] or anything like that, not saying that you should :), just an example), then the view didn't reach 0 (zero) retain count and is still there - which means its subviews aren't removed
The proper way to do it is to save the reference to the controller as an instance variable (usually declare a synthesized property), and release it only when you are done with the view, making sure that the view is removed from superview before hand. The default templete for a View Based App shows how view controller should be managed
Hope this helps to understand why both methods behave differently
Based on your clarifications, you don't need secondView property or iVar. Also in your loadSecond instead of secontController = bla you need self.secondController = bla, otherwise you simply assign reference to the iVar instead of going through the setter.
Yes, it's possible to load subviews/other resources from a nib without having a dedicated controller
This is how you do it (one of the approaches):
UIView *result = nil;
NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:#"MyNibName" owner:owner options:nil];
for ( id o in bundle ) {
if ( [o isKindOfClass:[UIView class]] ) {
result = (UIView *)o;
break;
}
}
Here the result will contain the first UIView in MyNibName. You can use other criteria to find out whether you got the view you wanted (tags, types...)
I'm adding a subview to my primary iPad UIViewController, and within that subview I need to reference said view controller in order to play a video using that controller.
Can anyone help me out with the way that should be done, and possibly a code example?
Thank you in advance.
Regards,
Ed
EDIT (A LITTLE BIT MORE INFO):
This subview is a view from a uiviewcontroller class that is designed for the iPhone. It's a table that loads a video when a row is pressed. The movieplayer loads the video within the referenced viewcontroller (which is why I want to reference the iPad view controller from within the subview). The view is basically used within an iPad app in it's same form.
This sounds like an architecture problem. It's not up to your view to tell something to play a sound. That's a controller's job. It's up to your view to tell "someone" that it was touched, or slid, or whatever the user has done. "Someone" (who is watching) will then perform the correct response to that.
To do this, your view should generally take a target, and possibly and action. Look at how UIControl (for example, UIButton) informs other objects that it has been activated. The observer (controller) then reacts accordingly.
EDIT
The view should not load a video. A view controller should load the video and install it into the correct view. The only job the view has is to tell its view controller that it has been pressed. UITableView handles this automatically with the UITableViewDelegate method tableView:didSelectRowAtIndexPath:. If you're not using a UITableView, you should still follow this pattern. The view accepts has a delegate and tells the delegate (controller) that something was selected. Then the controller updates the views with the new data.
You probably already have the main view living in a property of your application delegate (it is commonly assigned via the application's .xib file, look in the app delegate's applicationDidFinishLaunching: method, where it adds the main view as a subview to the window, something like:
[window addSubview:primaryController.view];
[window makeKeyAndVisible];
).
So anywhere in your app where you need to access the main view, you can do:
[(MyAppDelegateType*)([UIApplication sharedApplication].delegate).primaryController somePrimaryControllerMethod];
Edit: while this will work, I agree with Rob Napier that this isn't the best way to do it, architecture-wise.
You could just use [yourSubview superview] to get the superview.
Can't you just add a property to the view like this:
#property (nonatomic, assign) UIViewController *parentViewController;
And on initialization of the view (from within the UIViewController), set the property:
UIView *customView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
customView.parentViewController = self;
And from then on you should be able to call the parentViewController from within the view.
Edit: Rob Napier has a good point, it's probably a better idea to set a target and selector from within the view instead of a view controller. This way you should be able to hook up methods directly with the view. The properties would look like this:
#property (nonatomic assign) id target;
#property (nonatmic, assign) SEL selector;
And perhaps add a designated initializer to your view:
- (id)initWithFrame:(CGRect)aFrame target:(id)aTarget selector:(SEL)aSelector
{
self = [super initWithFrame:aFrame];
if (self)
{
self.target = aTarget;
self.selector = aSelector;
}
return self;
}
This question has been asked a lot e.g. here but as far as I can see is yet to be answered in full.
I have a UITabBarController with a UINavigationController as the root vc for one of the tabs, which itself has a MKMapView as its root vc. The behaviour I want is for the map to partially curl upwards, while leaving the tab bar in place (similar to the Maps app).
So far all I have managed to get working is for the whole view to curl, which isn't as nice.
Solutions I have seen are to set the hidesBottomBarWhenPushed property to NO, which would make sense however this doesn't seem to work, (unless I am doing something wrong).
For clarity, my code is as follows:
MyVC *aView = [MyVC init];
aView.modalTransitionStyle = UIModalTransitionStylePartialCurl;
aView.hidesBottomBarWhenPushed = NO;
For the presenting part, I have tried the two alternatives below, neither of which seem to work:
[self presentModalViewController:updateStatus animated:YES];
[[self navigationController] presentModalViewController:updateStatus animated:YES];
Any help much appreciated.
I've scoured StackOverflow (and the Internet) for a solution to this problem. The question has been asked many times, but as you note, never sufficiently answered. Many solutions give an acceptable solution if it is unimportant whether, e.g., a lower toolbar curls up as well.
Others have provided a solution using UIView animations / CoreAnimation rather than UIModalTransitionStylePartialCurl as a modal transition style; this is at worst a solution not allowed in the App Store, and at best is not quite the same effect as one gets from UIModalTransitionStylePartialCurl (e.g. the shape of the curl is different).
None of these solutions have provided an answer that mimics Apple's solution in the Maps app (i.e., using UIModalTransitionStylePartialCurl but leaving an un-curled UIToolbar at the bottom of the screen).
I will continue in this tradition of incomplete answers, since you ask about a UITabBarController and my solution doesn't specifically address that case. It does, however, solve the problem I had, which was to get a half page curl with an un-curled toolbar at the bottom.
There must be a more elegant way to do this, but this is how I managed.
The rootViewController of my AppDelegate is a subclass of UIViewController, which I'll call TAContainerViewController. TAContainerViewController manages a) the actual contents of the screen (the "stuff to be curled"), TAContentViewController, and b) the contents "behind" the TAContentViewController (e.g. settings), which I'll call TAUnderCurlViewController.
My instance of TAContainerViewController had properties for a TAContentViewController and a TAUnderCurlViewController. The UIView that was my content was a subview of TAContentViewController's view property; likewise what the user sees under the curl is the view property of the TAUnderCurlViewController.
In the init method of TAContainerViewController I make sure to do the following:
_underCurlVC.modalTransitionStyle = UIModalTransitionStylePartialCurl;
And to curl the contents to reveal under the page, I set up an action that calls this code:
[self.contentVC presentModalViewController:self.underCurlVC animated:YES];`
where self is the TAContainerViewController, contentVC is an instance of TAContentViewController, and underCurlVC is an instance of TAUnderCurlViewController.
To dismiss the view, simply [self.contentVC dismissModalViewControllerAnimated:YES];.
Some strangeness seems to occur with the frame of contentVC when the modal view is dismissed, so I manually reset the frame when the modal view is dismissed.
I've posted a sample project with more details on Github. Hopefully someone can take this and turn it into a slightly more elegant solution, or expand it to work with a UINavigationController or UITabBarController. I think the trick is to pull the View Controllers out of the well-defined relationships in the Cocoa subclasses, so maybe subclassing those specialty View Controllers would do it.
Tim Arnold's response worked great for me, thanks!
One trap to watch out for: your modal page-curl transition will take over the whole screen if your content view controller is added as a child of the container view controller. You could just not add it as a child, but then none of the view lifecycle methods will get called on your content controller (e.g. viewDidLoad, viewWillAppear), which could be a problem.
Fortunately, there is a way around this. In your container controller:
Add your content controller as a child in viewDidLoad
Remove it as a child in viewDidAppear
Re-add it as a child in viewWillDisappear.
That way, your content controller gets its lifecycle methods called, while still being able to do a modal page-curl transition without taking up the whole screen.
Here is the entire code of a bare-bones solution:
#interface XXContainerController : UIViewController
#property (strong, nonatomic) UIViewController *contentController;
#property (nonatomic) BOOL curled;
#end
#implementation XXContainerController
#synthesize contentController = _contentController;
#synthesize curled = _curled;
- (void)viewDidLoad
{
[super viewDidLoad];
self.contentController = [self.storyboard
instantiateViewControllerWithIdentifier:#"SomeControllerInStoryboard"];
// Add content controller as child view controller.
// This way, it will receive all the view lifecycle events
[self addChildViewController:self.contentController];
self.contentController.view.frame = self.view.bounds;
[self.view addSubview:self.contentController.view];
[self.contentController didMoveToParentViewController:self];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Remove the content controller as child view controller.
// This way, the modal page curl transition will
// not take over the whole screen.
// NOTE: need to wait until content controller has appeared
// (which will happen later).
// Achieve this by running the code at the end of the animation loop
[UIView animateWithDuration:0 animations:nil completion:^(BOOL finished) {
[self.contentController removeFromParentViewController];
}];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Add the content controller as child view controller again
// so it receives the view lifecycle events
[self addChildViewController:self.contentController];
}
- (void)setCurled:(BOOL)curled
{
if (curled == _curled) return;
_curled = curled;
// Curl up the content view and show underneath controller's view
if (curled) {
// Note you can specify any modal transition in storyboard
// E.g. page curl, flip horizontal
[self.contentController
performSegueWithIdentifier:#"SomeModalSegueDefinedInStoryboard"
sender:self];
// Uncurl and show the content controller's view again
} else {
[self.contentController dismissModalViewControllerAnimated:YES];
// Have to do this, otherwise the content controller's view
// gets messed up for some reason
self.contentController.view.frame = self.view.bounds;
}
}
#end
I have a TabBarController with 7 Custom ViewControllers and what i am trying to do is have the TabBarController on startup load the first ViewController in its array as well as a ViewController in one of the other tabs.
As far as i understand it the viewDidLoad method for the ViewController's is only called for that tab when it is first selected. Is there a way to force the TabBarController to call a ViewController viewDidLoad method even if its not active?
thx
Just reference the ViewController view:
[myViewController view]
If myViewController's view is nil, then it will be loaded.
Note that even if this approach is working, your app will be affected by the view loading/unloading mechanism which is controlled by the internal run loop logic and not by your app, while the view controller "internal logic" should be initialized by the initWithNib: method which is completely under your control. But this is just a suggestion to avoid strange bugs, anyway the solution proposed is working.
This technique seems to work pretty good. I have an app with a UITabBarController at the bottom with 5 buttons in it. When the user clicks the 3rd button, the viewDidLoad for that view took 5 seconds to do stuff so I used this technique to cause its viewDidLoad to get called when the app starts. I don't want the 3rd view to be displayed; just to be initialized so it shows up instantly when clicked.
I had to subclass the UITabBarController to something like FoohBarController, then in it's viewDidLoad I made a background thread do this:
{
// get a pointer to the 3rd item in the tab bar (0 based)
UINavigationController *navcon = [self.viewControllers objectAtIndex:2];
// get a pointer to the viewController I want to init
CalendarViewController *calendar = [navcon.viewControllers objectAtIndex:0];
// Just ask for the view. This will force the view to load and to initialize
UIView *v = calendar.view;
v = nil; // to remove a compiler warning
}
I have a viewcontroller, where I load subviews on. In those subviews I have a UIButton which will show a modalViewController. If I call the function on the main viewcontroller, I get the modal exactly in the middle as I want them to be, but if I call the exact same method in the view I've added as a subview the modal is presented somewhere on the right side of the screen. So now is my question: how can I present the modal on the super view instead? Or how do I put the modalview in center? Because the views I've added as subviews contains data which the modalpopup needs.
This is how I present the modal:
MapViewer *map = [[MapViewer alloc] init];
map.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentModalViewController:map animated:YES];
[map release];
Hope somebody can help me with this.
Thanks in advance!
Edit:
Maybe useful to be more clear:
AppDelegate.m
-> MainWindow.m
-> OtherViewController.m
So I add an x amount of OtherViewControllers as subviews to MainWindow. The OtherViewController contains the function mentioned above. But the map needs to be displayed as a modal on the MainWindow, and not on the OtherViewController so it gets nicely centered etc.
One thing you might do is set the target of the UIButton object to the main view controller using setTarget:action: method.
Ok I've managed to get it working using the notificationcenter so I can call a method on the MainWindow from other views. So if anyone is asking the same question, have a look at the notificationcenter :D