I'm trying to use the UIViewController containment feature in one of my projects. The app is meant to be used in landscape mode only.
I add UIViewController A as a child of UIViewController B and add A's main view as a subview of one of B's views. I'm also saving a reference to A in B:
#interface BViewController : UIViewController
#property (retain, nonatomic) AViewController *aVC;
#end
#implementation BViewController : UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.aVC = [self.storyBoard instantiateViewControllerWithIdentifier:#"A"];
[self addChildViewController:self.aVC];
[self.myContainerView addSubview:self.aVC.view];
}
#end
The problem I have is that landscape orientation is not being respected. I did some debugging and found a solution, but I fear is not ideal as it's more of a hack:
In B:
- (void)viewDidLoad
{
[super viewDidLoad];
self.aVC = [self.storyBoard instantiateViewControllerWithIdentifier:#"A"];
[self addChildViewController:self.aVC];
[self.myContainerView addSubview:self.aVC.view];
[self.aVC didMoveToParentViewController:self];
}
In A:
- (void)didMoveToParentViewController:(UIViewController *)parentVC
{
// Interchange width and height
self.view.frame = CGRectMake(self.view.frame.origin.x, self.view.frame.origin.y, self.view.frame.size.**height**, self.view.frame.size.**width**);
}
Am I missing something here?
Your code:
self.aVC = [self.storyBoard instantiateViewControllerWithIdentifier:#"A"];
[self addChildViewController:self.aVC];
[self.myContainerView addSubview:self.aVC.view];
was always wrong. You MUST send didMoveToParentViewController: to the child controller after adding it to the parent. See my discussion here:
http://www.apeth.com/iOSBook/ch19.html#_container_view_controllers
As for the rotation, it is probable that you're just doing all this too early. The app starts out in portrait and hasn't rotated yet to landscape when viewDidLoad is called. I give solutions to this problem here:
http://www.apeth.com/iOSBook/ch19.html#_rotation
Note the suggestion there that you wait until didRotateFromInterfaceOrientation: to finish setting up your view's appearance. I think you might be facing here the same issues I'm describing there.
Related
I am using storyboards to build my app's UI. Essentially, I am opening a UINavigationController as modal view, and in this navigation controller, I embed as rootViewController an instance of another UIViewController (Location Selection View). This is all set up in storyboard and looks basically like this:
Now, I want to access the navigation controller in the viewDidLoad of LocationSelectionViewController in order to include a UISearchBar in the navigation bar with:
self.searchDisplayController.displaysSearchBarInNavigationBar = YES;, this doesn't work however, because my UINavigationController is nil at this point, I know because I set a breakpoint and logged it:
(lldb) po self.navigationController
nil
Does anyone know why or what I have to do so that there is actually an instance of UINavigationController accessible on my LocationSelectionViewController?
UPDATE: Here is more code, the header really only consists of the declarations
LocationSelectionViewController.h
#protocol LocationSelectionViewControllerDelegate <NSObject>
- (void)setLocation:(Location *)location;
#end
#interface LocationSelectionViewController : UIViewController <GMSGeocodingServiceDelegate, UISearchBarDelegate, UISearchDisplayDelegate, UITableViewDataSource, UITableViewDelegate, GMSMapViewDelegate>
#end
Parts of LocationSelectionViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
self.searchBar.text = DUMMY_ADDRESS;
self.previouslySearchedLocations = [[CoreDataManager coreDataManagerSharedInstance] previouslySearchedLocations];
self.searchResults = [[NSMutableArray alloc] initWithArray:self.previouslySearchedLocations];
self.mapView.delegate = self;
self.gmsGeocodingService = [[GMSGeocodingService alloc] initWithDelegate:self];
self.searchDisplayController.displaysSearchBarInNavigationBar = YES;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self addMapView];
}
OK, I just solved my problem. I strongly believe it was a bug in interface builder. I deleted the old navigation controller and just dragged and dropped a new one onto the storyboard, now calling po self.navigationController in viewDidLoad actually returns an instance of UINavigationController. Thanks for all the help though, I appreciate it a lot!
as you can read in the title, I want to display an UIView from one UIViewController in another UIViewController.
The reason for this is, that I want to design the UIView in an extra UIViewController in the storyboard, but use it as an overlay view in my MainViewController on which the app mainly runs.
I tried the following:
In the MainViewController I created an instance of the second UIViewController and saved its view in the UIView i called overlayView.
UIViewController* rvc = [self.storyboard instantiateViewControllerWithIdentifier:#"overlayView"];
self.overlayView = rvc.view;
After that step, I thought I simply add it as a Subview, but sadly it didn't work.
[self.view addSubview:self.overlayView];
Is there anything missing or should I try something completely different ?
Thanks in advance :)
EDIT: Now theres the problem, that an Error called BAD_ACCESS occurs by pressing a button of the overlayView.
I created a ViewController, whose views alpha I regulated to a value of 0.9. On this view I added a simple button up to now. In my MainViewControllers viewDidLoad Method I used the following code to initialize the "new" ViewControllers view:
UIStoryboard* st = [UIStoryboard storyboardWithName:#"Main_iPhone" bundle:nil];
UIViewController* vc = [st instantiateViewControllerWithIdentifier:#"overlayView"];
self.overlayView = vc.view;
[self addChildViewController:vc];
self.overlayView.alpha = 0.0;
As a next step I "called" the overlayView to appear by pressing a button on the MainViewController and animated the alpha for a smooth fadeIn:
[self.view addSubview:self.overlayView];
[UIView animateWithDuration:1.3 animations:^{
self.overlayView.alpha = 0.9;
}];
Now if I press the button on the overlayView I want it to call a method of the MainViewController, which is doing some stuff and lets the overlayView disappear. For this case I used a NSNotificationCenter.
But by pressing this button (on the overlayView), the following error occurs in main.m:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x128a60cd0)
First I thought, the problem was caused by the NSNotificationCenter. So I tried to call the method using Delegation. But it didn't help.
Delegation Code:
OverlayViewController.h
#class OverlayViewController;
#protocol OverlayViewControllerDelegate
- (void) setToNormalAction;
#end
#interface OverlayViewController : UIViewController
#property (weak, nonatomic) id <OverlayViewControllerDelegate> delegate;
#property (strong, nonatomic) IBOutlet UIView *overlayView;
- (IBAction)buttonTouchUpInside:(id)sender;
#end
Button Method:
- (IBAction)buttonTouchUpInside:(id)sender
{
[self.delegate setToNormalAction];
}
In the MainViewController.h I implemented the created "OverlayViewControllerDelegate" as known and in .m I used the declared method to fadeOut the overlayView and do some other stuff.
Do you have an idea to resolve this ?
Thanks in advance
Try...
UIViewController* rvc = [self.storyboardinstantiateViewControllerWithIdentifier:#"overlayView"];
self.overlayView = rvc.view;
[self addChildViewController:rvc];
[self.view addSubview:self.overlayView];
You could just design it as a view in a nib then there's no need for an extra VC just to have somewhere to put it, and all the hassles that is going to bring.
There's nothing that says you can't continue to use nibs in addition to a having a storyboard.
I am presenting a modal view using a storyboard segue set as Form Sheet.
The problem is, when I rotate the iPad after this view is displayed, the view is removed from the view/dismissed.
I have no idea why. It only seems to occur when starting in Portrait then rotating to Landscape.
If I start in Landscape then show the view then rotate it stays on the screen fine.
Any ideas?
EDIT ----
It also seems that full screen modal views are also dismissed after rotation!
There's nothing special going on in the presentation code, this is a full screen modal:
EditViewController *editView = [self.navigationController.storyboard instantiateViewControllerWithIdentifier:#"editViewController"];
editView.delegate = self;
editView.image = image;
editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:editView animated:YES completion:nil];
This happens on both iOS 6 and iOS 7
EDIT 2 ----
Forgot to mention, i'm presenting the modal from the left/master view controller of a UISplitViewController
late, but what it worked for me was just before
[self presentViewController:aController animated:YES completion:nil];
dismiss the master controller, adding this lines
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAutomatic];
and then present your controller
Get rid of: editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
That will solve the issue you are experiencing. Modal view controllers presented on iPad as a Form Sheet do not rotate correctly using that transition style.
its really hard to get the cause how and why this is happening as i found that this also happen with UIPopover also as when you rotate it UIPopover hide because ???
So if you want to keep your view then just call again your controller after rotation will do fine user experience
This is not a bug its a limitation on UISplitViewController. The problem exists when the masterViewController (which is a UIPopoverController) is able to be dismissed. Heres how it works with the assumption that your app does allow the masterViewController to be dismissed in portrait and does not allow in landscape.
In portrait while the masterViewController is visible, if you were to present a modal from a viewController in the masterViewController and then rotate to landscape, the modal would disappear in iOS7 and the app would not rotate in iOS8. iOS8 introduces a condition to prevent the bad experience of iOS7. iOS7 losses the modal in the process of moving the masterViewController from the popoverController to a contained viewController in the splitViewController.
The modal needs to be presented from the splitViewController and not from the masterViewController. The only problem with this is the modal gets presented below the masterViewController in portrait. My solution is to dismiss the masterViewController and then present the modal.
There are several ways to achieve this result depending on how complex your code needs to be. Here's how I do this in my app.
I first subclass UISplitViewController in order to have a reference to the popoverController. I use delegate forwarding in order to access the delegate methods internally and externally. Heres the .h
// MainSplitViewController.h
#import <UIKit/UIKit.h>
#interface MainSplitViewController : UISplitViewController
#property (nonatomic, weak, readonly) UIPopoverController* primaryColumnController;
#end
And the .m
// MainSplitViewController.m
#import "MainSplitViewController.h"
#interface MainSplitViewController () <UISplitViewControllerDelegate>
#property (nonatomic, weak) id<UISplitViewControllerDelegate> externalDelegate;
#property (nonatomic, weak) UIPopoverController* primaryColumnController;
#end
#implementation MainSplitViewController
- (instancetype)init {
self = [super init];
if (self) {
self.delegate = self;
}
return self;
}
#pragma mark - Split View Controller Delegate
- (void)splitViewController:(UISplitViewController *)svc popoverController:(UIPopoverController *)pc willPresentViewController:(UIViewController *)aViewController {
self.primaryColumnController = pc;
if ([(id)self.externalDelegate respondsToSelector:_cmd]) {
[self.externalDelegate splitViewController:svc popoverController:pc willPresentViewController:aViewController];
}
}
#pragma mark - Delegate Forwarder
- (void)setDelegate:(id<UISplitViewControllerDelegate>)delegate {
[super setDelegate:nil];
self.externalDelegate = (delegate != self) ? delegate : nil;
[super setDelegate:delegate ? self : nil];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [super respondsToSelector:aSelector] || [delegate respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [delegate respondsToSelector:aSelector] ? delegate : [super forwardingTargetForSelector:aSelector];
}
#end
Next I create a class extension on UIViewController
// UIViewController+Popover.h
#import <UIKit/UIKit.h>
#interface UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController;
#end
And the .m
// UIViewController+Popover.m
#import "UIViewController+Popover.h"
#import "MainSplitViewController.h"
#implementation UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController {
UIViewController* viewController = self;
if ([self.splitViewController isKindOfClass:[MainSplitViewController class]]) {
viewController = self.splitViewController;
MainSplitViewController* mainSplitViewController = (MainSplitViewController *)self.splitViewController;
if (mainSplitViewController.primaryColumnController.popoverVisible) {
[mainSplitViewController.primaryColumnController dismissPopoverAnimated:YES];
}
}
return viewController;
}
#end
Now where ever you present the modal, instead of calling [self presentViewController: ... call [self.popoverPresentingViewController presentViewController: ...]. Remember to import UIViewController+Popover.h
your question came closest to my bug, On returning from modalView the parentView will switch to orientation in which the application was opened.
Visually it appears that the modal view is rotated and then returns.
I solved it by removing the modal view altogether, and using
[self.navigationController pushViewController: <the View(not modal now)>]
instead of using-
[self presentViewController:<Modal View>]
I think this is because the navigation controller doesn't own the Modal View, hence it reloads - when returning from the modal view - to incorrect orientation
Problem:
When presenting a view controller modally, it gets dismissed on rotation.
Approach:
Set the UISplitViewControllerDelegate
Use the UISplitViewControllerDelegate methods
Hold a reference to your modal view controller in an instance variable
Check if your modal view controller's presenting view controller exists.
If it exists, nothing needs to be done, else just present without any animation.
UISplitViewControllerDelegate methods:
func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
Note:
UISplitViewControllerDelegate has quite a methods, it can be daunting initially, if you spend some time experimenting, you can achieve what you want.
It has fine grained access.
I'm very late but try this. It works for me.
[self.splitViewController presentViewController:editView animated:YES completion:nil];
I have setup a basic test app that displays a view containing a label, with no use of IB. I want to use a custom UIView subclass AND custom UIViewController subclass.
This will run as anticipated, but the MyViewController's viewWillAppear and other similar delegates do not fire.
What am I missing to make these fire? In previous projects (using IB), these would fire just fine.
Here is the complete code:
AppDelegate - loads a 'MainVC' view controller and sets it as the root controller
#import "AppDelegate.h"
#import "MainVC.h"
#implementation AppDelegate
#synthesize window = _window;
#synthesize mainVC = _mainVC;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.mainVC = [[MainVC alloc] init];
self.window.rootViewController = self.mainVC;
[self.window makeKeyAndVisible];
return YES;
}
MainVC - creates a 'MyViewController' which allocates the 'MyView' (it also passes down the frame size that should be used for the view)
#import "MainVC.h"
#import "MyViewController.h"
#implementation MainVC
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
MyViewController *controller = [[MyViewController alloc] init];
CGRect frame;
frame.origin.x = 5;
frame.origin.y = 5;
frame.size.width = self.view.frame.size.width - (2 * 5);
frame.size.height = self.view.frame.size.height - (2 * 5);
controller.startingFrame = frame;
[self.view addSubview:controller.view];
}
return self;
}
MyViewController - creates the MyView
#import "MyViewController.h"
#import "MyView.h"
#implementation MyViewController
#synthesize startingFrame;
- (void)loadView{
self.view = [[MyView alloc] initWithFrame:startingFrame];
}
- (void)viewWillAppear:(BOOL)animated{
NSLog(#"appearing"); //doesn't do anything
}
- (void)viewDidAppear:(BOOL)animated{
NSLog(#"appeared"); //doesn't do anything
}
MyView
#import "MyView.h"
#implementation MyView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
label = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 150, 40)];
[label setText:#"Label"];
[self addSubview:label];
}
return self;
}
Your mistake: You're setting a root view controller and then adding another's view controller view on top of that. While the second view is added to the view hierarchy, its view controller remains "unwired" this way. In fact if you check on your MainViewController's parentViewController, you will notice it's nil.
Why: The viewWillAppear method will be sent only to the root view controller or to view controllers in the hierarchy of the root view controller (those that were presented using presentModalViewController:animated: or presentViewController:animated:completion:).
Solutions: to solve it you have a few options:
Use your view controller as the root view controller
Present your view controller through one of the methods mentioned above
Keep your code as it is and manually wire those events to child view controllers (beware of this method though, as I believe the events you mention are automatically forwarded under iOS 5 - you can easily check this out).
If I recall properly another way to make these event get forwarded to your view controller is to add your view controller's view to the window, rather than to the parent view.
There's a number of very basic things that went wrong:
you're doing your whole setup in initWithNibNamed: for your MainViewController, yet you're creating it calling just init. So your setup will never happen
you're implementing a second VC (MyViewController), apparently just to create myView, which you then add to your rootVCs hierarchy. Not good! Only a single VC (in your case MainViewController) should be responsible to create and manage the views in its hierarchy
don't do VC controller setup in loadView, like you did in MyViewController. In your case it is the only way to make things work, because MyVC never actually gets fully up and running, but the approach is wrong - you're basically forcing the View Controller to set up the view, although the controller itself is never in control of anything
There's a few more things, but those are the most important ones - it appears like it would be a good idea for you to read about the whole basic concept of the Model - View - Controller concept again. Next, you should be digging through the class references for both UIViewController and UIView.
Even if you would get the results you desire at last using your current approach, it wouldn't help you in the long run, because you wouldn't learn to use the involved elements properly.
Methods are not invoked on a view controller inside another view controller. If developing for iOS 5 only then check out UIViewController Containment which can be used to solve this. If you want your application to be compatible with previous iOS versions you can forward method invocations to your child view controller. Personally I prefer subclassing SFContainerViewController which handles this automatically: https://github.com/krzysztofzablocki/SFContainerViewController
I am tryin to display multiple UIViewController objects inside a single view. For the time being I want to display a single UIViewController object when the app loads. But the app screen appears blank, while it should be displaying a label inside the child view controller.
Here is what I did:
ParentViewController.h
#import <UIKit/UIKit.h>
#interface ParentViewController : UIViewController
{
UIViewController *child1Controller;
UIViewController *child2Controller;
}
#end
ParentViewController.m
#import "ParentViewController.h"
#import "Child1Controller.h"
#import "Child2Controller.h"
#interface ParentViewController ()
#end
#implementation ParentViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { ... }
- (void)viewDidLoad
{
child2Controller = [[Child2Controller alloc] init];
[self.view addSubview:child2Controller.view];
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)viewDidUnload { ... }
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { ... }
#end
Then in the storyboard in interface builder
add 3 view controllers
assigned a class to each one of them ParentViewController, Child1Controller & Child2Controller
in Child2Controller object, added a UILabel inside View.
in Child2Controller.h defined the IBOutlet for UILabel and added a synthesize statement for the same in Child2Controller.m
finally in project-Info.plist set the main storyboard file
Did I miss something over here?
Starting from iOS 5 it's possible to take advantage of View Controller Containment. This is a new methodology that allows you to create a custom controller container like UINavigationController or UITabBarController.
In your case, this could be very useful. In fact, in your storyboard you could create the parent controller and the two child controllers. The parent could be linked to another scene while the two children are not linked. They are independent scenes that you can use within your parent controller.
For example in viewDidLoad method of your parent controller you could do the following:
- (void)viewDidLoad
{
[super viewDidLoad];
UIStoryboard *storyboard = [self storyboard];
FirstChildController *firstChildScene = [storyboard instantiateViewControllerWithIdentifier:#"FirstChildScene"];
[self addChildViewController:firstChildScene];
[firstChildScene didMoveToParentViewController:self];
}
Then in your FirstChildController override didMoveToParentViewController
- (void)didMoveToParentViewController:(UIViewController *)parent
{
// Add the view to the parent view and position it if you want
[[parent view] addSubview:[self view]];
CGRect newFrame = CGRectMake(0, 0, 350, 400);
[[self view] setFrame:newFrame];
}
And voilĂ ! You have a controller that contains one view that is managed by a child controller.
For further info see how-does-view-controller-containment-work-in-ios-5.
Hope it helps.