I'm currently trying to make a small demo app which is supposed to present 2 views at the same time :
- The first one, BaseViewController, is a classic controller with 2 buttons
- The second one, OverViewController, is launch on top of baseView and contains
a last button, and is supposed to be transparent and sending the touch controls
to the first one
Both viewController have their xib. To make it clear : rootView --> baseView + overView (transparent)
My problem is NOT to send the control events from the 2nd view to the first, but to make the 2nd view transparent and functionnal.
Here's what I've tried so far :
1) ------------ Presenting the two view controllers ------------
From the app rootViewController :
- (IBAction)buttonClicked:(id)sender
{
OverViewController *overVC = [[OverViewController alloc] init];
[overVC presentViewControllerFrom:self];
}
From OverViewController :
- (id)init
{
self = [super init];
if (self)
{
self.base = [[BaseViewController alloc] initWithNibName:#"BaseViewController" bundle:nil];
}
return self;
}
I made a custom method to make overView present the baseView before showing up.
- (void)presentViewControllerFrom:(RootViewController *)sender
{
[sender presentViewController:self.base animated:NO completion:nil];
self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.0];
self.view.opaque = NO;
[self.base presentViewController:self animated:YES completion:nil];
}
At this point, the baseView appears, followed by the overView. The button of overView work correctly. Great ! But then the background of overView doesn't show what's supposed to be behind and stays black. No alpha transparency.
2) ------------ Presenting the baseView and adding overView as a subview ------------
From the app rootViewController (same than the one before) :
- (IBAction)buttonClicked:(id)sender
{
OverViewController *overVC = [[OverViewController alloc] init];
[overVC presentViewControllerFrom:self];
}
From OverViewController (not presenting but adding subview) :
- (void)presentViewControllerFrom:(RootViewController *)sender
{
[sender presentViewController:self.base animated:NO completion:nil];
self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.0];
self.view.opaque = NO;
[self.base.view addSubview:self.view];
}
This time, the transparency is great but any touch on the overView button cause a EXC_BAD_ACCESS to show up.
There I am, wondering how to do this. Any help will be greatly appreciated ! :)
Don't use a combination of presenting a view controller and adding subviews. Choose 1. It should be subview that you choose. It can be owned by another controller and that controller should probably be added as a child view controller.
In your first situation you see black because iOS is removing the (now expected to not be visible) view from the view hierarchy. So your view is transparent but what you expect to be behind it is no longer there.
Your second situation is probably just resulting in an invalid view hierarchy somewhere.
The overview controller should not own the base controller. The base controller should be shown and then the overview controller view added as a subview.
Related
I'm building an iOS app using a NavigationController. However, in this app I need a sub navigation bar and it needs to be in every view. I initially implemented this using a toolbar in every view. But what happens, when a new view slides in, is that the toolbar slides in as well. I need the toolbar to be persistent, like the navigation bar. What's the best way to approach this?
My best guess is to set [self.navigationController setToolbarHidden:YES animated:YES]; to YES and then somehow positioning that underneath the navbar...
This is how I used to do it in every viewcontroller:
#interface TableViewController ()
#property (weak, nonatomic) IBOutlet UIToolbar *mainToolBar;
#end
#implementation TableViewController
- (void)viewWillAppear:(BOOL)animated
{
[self.navigationController setToolbarHidden:YES animated:YES];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = #"TV Shows";
[self addRightMenuButton];
[self loadNinjas];
self.mainToolBar.barTintColor = [UIColor whiteColor];
self.mainToolBar.layer.shadowColor = [[UIColor blackColor] CGColor];
self.mainToolBar.layer.shadowOffset = CGSizeMake(1.0f, 1.0f);
self.mainToolBar.layer.shadowRadius = 3.0f;
self.mainToolBar.layer.shadowOpacity = 1.0f;
self.tableView.dataSource=self;
self.tableView.delegate=self;
}
Three distinct approaches I can think of trying:
Make a taller Navigation Bar subclass with your secondary toolbar thing at the bottom. Set it as the Navigation Controller's Navigation Bar. I've tried this a couple of times to achieve different things and it didn't work out, but maybe it will for you.
Stick with what you're doing, a second toolbar on every VC, and use a custom VC transition or presentation controller to make it appear like the toolbar isn't moving while the rest of the content is.
Don't do this. I can't really see a way that a double navigation thing is going to be good for an iPhone app.
UPDATED: Fixed via "screenshot" method outline below. This works but is there a more elegant way?
How would I go about dismissing a stack of modal view controllers with animation without flashing on screen any of the presented VCs between the top and bottom? Trying to do this with animation isn't working. See code below and inline comments describing my issue. You can copy/paste this code into a new project in Xcode to see for yourself if you'd like!
//
// ViewController.m
// MultipleModals
//
#import "ViewController.h"
#import "MyViewController.h"
#import "MyHelper.h"
#interface ViewController ()
#end
#implementation ViewController
static BOOL doAgain = YES; // So when red appears again, we don't endlessly cycle (for testing)
- (void)viewDidAppear:(BOOL)animated
{
// Invoke super
[super viewDidAppear:animated];
// Prevent loop when we dismiss all the way back to red (for testing)
if (doAgain)
{
// Okay here's where the demo code starts...
// PRESENTING a full stack of modals WITHOUT animation WORKS and results in the user
// only seeing orange when this red view controller "appears" (red never actually appears, which is great)...
MyViewController *purple = [[MyViewController alloc] init];
purple.title = #"purple"; // For use in MyViewController's dealloc method
purple.view.backgroundColor = [UIColor purpleColor];
[self presentViewController:purple animated:NO completion:^{ // Purple successfully gets presented and the user never sees purple, great.
NSLog(#"Purple?");
MyViewController *green = [[MyViewController alloc] init];
green.view.backgroundColor = [UIColor greenColor];
green.title = #"green"; // For use in MyViewController's dealloc method
[purple presentViewController:green animated:NO completion:^{ // Green successfully gets presented and the user never sees green, great.
NSLog(#"Green?");
MyViewController *orange = [[MyViewController alloc] init];
orange.view.backgroundColor = [UIColor orangeColor];
orange.title = #"orange"; // For use in MyViewController's dealloc method
[green presentViewController:orange animated:NO completion:^{ // Orange successfully gets presented and the user DOES see orange, great.
NSLog(#"Orange?");
// FIXED MY ISSUE STARTING HERE
// Comment out the following code to toggle between
// the "flashing purple issue" and "the desired outcome" (single
// animation from top to bottom regardless of how many VCs are
// on the stack, i.e. no flashing).
// Get orange screenshot
UIImage *orangeScreenShotImage = [MyHelper screenshot];
UIImageView *orangeScreenShotImageView = [[UIImageView alloc] initWithImage:orangeScreenShotImage];
// Give purple an orange screenshot since orange will just "flash away" and then purple will animate
// away but we'll disguise purple to appear as if it's orange by layering a screenshot of orange on purple. Boom.
[purple.view addSubview:orangeScreenShotImageView];
// FIXED MY ISSUE ENDING HERE
// FOR TESTING PURPOSES... dismiss after 5 seconds...
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
doAgain = NO; // Prevent viewDidAppear loop (related to my testing code)...
// THIS IS MY BUG HERE. WHEN I WANT TO **ANIMATE** THE DISMISSAL OF ORANGE ALL THE WAY BACK TO RED, HOWEVER, I SEE PURPLE FOR A FLASH BEFORE RED!! WHY?
// If I do not animate, things work as expected and I go from orange directly back to red in one flash. Why can't I go from orange back red WITH ANIMATION without seeing a flash of purple?
BOOL animateDismissalOfOrangeBackToRed = YES; // YES causes me to see a flash of purple before red, why?
[self dismissViewControllerAnimated:animateDismissalOfOrangeBackToRed completion:^{
NSLog(#"Back to red...");
}];
});
}];
}];
}];
}
}
- (void)viewDidLoad
{
// Invoke super
[super viewDidLoad];
// Set self's background color
self.view.backgroundColor = [UIColor redColor]; // Color of self, root VC
}
#end
MyViewController.m (enables us to have a custom dealloc method for debugging)
//
// MyViewController.m
// MultipleModals
//
#import "MyViewController.h"
#interface MyViewController ()
#end
#implementation MyViewController
- (void)dealloc
{
NSLog(#"Inside dealloc self.title = %#", self.title);
}
#end
UPDATED:
Added new MyViewController.m file for dealloc debugging.
Interestingly, the log comes out looking like this:
2014-11-20 10:06:28.847 MultipleModals[5470:946774] Purple?
2014-11-20 10:06:28.851 MultipleModals[5470:946774] Green?
2014-11-20 10:06:28.853 MultipleModals[5470:946774] Orange?
2014-11-20 10:07:04.055 MultipleModals[5470:946774] Inside dealloc self.title = orange
2014-11-20 10:07:04.056 MultipleModals[5470:946774] Inside dealloc self.title = green
2014-11-20 10:07:04.565 MultipleModals[5470:946774] Back to red...
2014-11-20 10:07:04.566 MultipleModals[5470:946774] Inside dealloc self.title = purple
UPDATED:
I've added a sample project so you can observe this first handle very easily if you'd like: https://github.com/johnerck/MultipleModals
Also, I have read Presenting View Controllers from Other View Controllers many times. They even say, "For example, if the user cancels the current operation, you can remove all objects in the chain by dismissing the first presented view controller. Dismissing a view controller dismisses not only that view controller but also any view controllers it presented." I am seeing this behavior but animation is displaying 3 views in total, not the expected 2.
It is not a bug. You change the removal animation only for self.presentedViewController("purple") when dismissViewControllerAnimated but not for all nested VCs. When "purple" receives a message about deleting with animation, it deletes all presentedViewController without animation. And then you see the animation of the "purple" without nested controllers. To check this you can simply create your own VC class and check -dealloc method.
I have a UINavigationController (NC) containing a UITableViewController (TVC0). When the user taps a row, it loads a UIPageViewController (PVC), which pages back and forth between other UITableViewControllers (TVC1).
TVC0 shows up inside NC (meaning it doesn't hide behind the navigation bar at the top or the tab bar at the bottom). When it pushes PVC, the first TVC1 appears inside the bounds of the nav bar and tab bar. However when I swipe, the TVC1s inside are hidden behind the navigation bar and tab bar. I can pull to reveal the contents, but when I release, it snaps back to behind the bar.
How can I force everything to appear between the two bars? I can't use storyboard (because it's a legacy app) and the embed in... option isn't available.
[Edit]
I added some logging and discovered that my embedded TVC1s frame has an absolute origin of 0, 64, but as soon as I tap, it goes to 0, 0. If I can't figure out a real solution, I can always fake it by adding 64, but I'd much rather figure out what's actually wrong.
[/Edit]
[More Edit]
I was testing another area in the iOS 6 simulator and discovered that this paging works flawlessly in iOS 6. So the issue I'm seeing is iOS 7 specific.
[/More Edit]
Here is my TVC0 viewDidLoad, PVC pageViewController:viewControllerBeforeViewController:, and a helper viewControllerAtIndex::
- (void) viewDidLoad
{
[super viewDidLoad];
NSDictionary* options = [NSDictionary dictionaryWithObject:
[NSNumber numberWithInteger: UIPageViewControllerSpineLocationMin]
forKey:
UIPageViewControllerOptionSpineLocationKey];
self.pageController = [[UIPageViewController alloc] initWithTransitionStyle:
UIPageViewControllerTransitionStyleScroll
navigationOrientation:
UIPageViewControllerNavigationOrientationHorizontal
options: options];
self.pageController.dataSource = self;
self.pageController.view.frame = self.view.frame;
NSArray* viewControllers =
[NSArray arrayWithObject: [self viewControllerAtIndex: self.initialIndex]];
[self.pageController setViewControllers: viewControllers
direction: UIPageViewControllerNavigationDirectionForward
animated: NO
completion: nil];
[self addChildViewController: self.pageController];
[self.view addSubview: self.pageController.view];
[self.pageController didMoveToParentViewController: self];
for (UIGestureRecognizer* recognizer in self.pageController.gestureRecognizers)
{
if ([recognizer isKindOfClass: [UITapGestureRecognizer class]])
{
recognizer.enabled = NO;
}
}
}
// SearchResultsList is TVC1
- (SearchResultsList*) viewControllerAtIndex: (NSUInteger) index
{
if (index >= self.items.count)
{
return nil;
}
SearchResultsList* retVal = [[SearchResultsList alloc]
initWithNibName: #"SearchResultsList" bundle: nil];
MyListItem* myItem = [self.items objectAtIndex: index];
MyMatchesRequest* matches = [[MyMatchesRequest alloc] initWithItemId: myItem.itemId];
[matches execute: ^(MySearchResults* results)
{
retVal.tableData = [NSMutableArray arrayWithArray: results.items];
retVal.view.frame = self.view.frame;
retVal.myItem = myItem;
retVal.index = index;
self.title = myItem.displayText;
[[retVal tableView] reloadData];
}];
return retVal;
}
- (UIViewController*) pageViewController: (UIPageViewController*) pageViewController
viewControllerBeforeViewController: (UIViewController*) viewController
{
SearchResultsList* vc = (SearchResultsList*)viewController;
if (vc.index == 0)
{
[self.navigationController popViewControllerAnimated: YES];
return nil;
}
return [self viewControllerAtIndex: vc.index - 1];
}
I had a very painful learning experience with similar behavior :(
Put this in your view controller's init:
self.automaticallyAdjustsScrollViewInsets = NO;
This is a new UIViewController property that defaults to YES in iOS 7
UIViewController Docs
Because you're presenting view controllers in a container which is less than the full size of the screen you need to set
self.pageViewController.definesPresentationContext = YES;
viewControllerWhichIsApageInPageController.modalPresentationStyle = UIModalPresentationCurrentContext
You code seems a little confused. You say it is your Page View Controller's viewDidLoad, yet it creates a new PVC and adds it as a child view controller to itself as parent. If you are really doing this, you are creating a PVC inside another PVC. I doubt this is what you want.
This code really belongs in TVC0 which invokes the PVC when the user taps a row. This invocation wouldn't be correct in viewDidLoad, but might sit nicely in a didSelectRowAtIndexPath method. Instead of bringing it in as a child controller, we can simply push it onto our navigationController's stack. (I expect you are doing this anyway in your outermost PVC).
But just in case, I would remove these three lines:
[self addChildViewController: self.pageController];
[self.view addSubview: self.pageController.view];
[self.pageController didMoveToParentViewController:self];
and replace them with
[self.navigationController pushViewController:self.pageController
animated:YES];
(this is called from your TVC0 - all of the code you have shown can live in TVC0)
Then to prevent the behaviour you describe, when you create your UINavigationController it should suffice to set the translucent property of it's navigationBar to NO.
update
I have looked at this issue in sufficient detail to see some buggy behaviour as you describe occur in some circumstances, but it's fairly hard to replicate.
The cause of the 'jump-up' behaviour is clear. If you have a translucent navBar, and it's automaticallyAdjustsScrollViewInsets is set to YES, in certain situations this can result in a private subview of the pageViewController (_UIQueuingScrollView) setting it's contentOffset.y to -64 when the pageViewController is first loaded. However as soon as the pageVC gets a chance to update itself (for example by a swipe or other touch gesture) it resets it's internal subviews state, losing that rogue contentOffset. So when you touch the first page, it jumps up. Where it then stays. This may be considered a bug, although it may be the result of misusing the pageViewController.
Just setting automaticallyAdjustsScrollViewInsets to NO doesn't cure your ills, as then your pages are all positioned behind the navBar. What you need to do is adjust the frame of the pageViewController itself.
I could go in to more detail, but at that point it gets hard to apply answers to your specific case as your app design looks a little odd, which is probably contributing to the issue.
if you can set your navigation controllers' navigation bar's translucent property to NO the problem should go away.
as I mentioned earlier, I cannot replicate your issue exactly as you describe, so I think you have not given a full and clear picture of your app design
loading a pageViewController as a child of a tableViewController, and making it's view a subview of the tableView, is a very odd way to go, and is doubtless contributing to your problems.
In viewControllerBeforeViewController the pageViewController child seems to pop it's parentViewController (the tableViewController) - so you would never see it's table contents? Either there is more relevant detail to the app, or you haven't described it accurately.
I recommend you first deal with these issues. Your problem may well then disappear.
i'm implementing a project with XCODE 5 and storyboard. I have added a view that is a UIPageViewController and when i alloc the first page (for this pageviewcontroller) the view appears normally but from second page the view appears under the navigation bar. What is the matter?
If i set this property
self.navigationController.navigationBar.translucent = NO;
for the view displayed the problem is solved but when i go back from Page View Controller to previous View, the buttons on it are shifted down.
Why? What is the solution?
Furthermore if i set translucent = NO the views displayed in storyboard are different from views displayed in my app (the views are not shifted in storyboard).
I can't post images because i'm not authorized.
This is the code of my PageViewController
- (void)viewDidLoad
{
[super viewDidLoad];
variabiliGlobali = [foaGlobalVariable sharedInstance];
variabiliGlobali.giornataCalID = 1;
numeroGiornate = [variabiliGlobali.calendario count];
self.dataSource = self;
foaGiornataViewController *initialView = [[foaGiornataViewController alloc] init];
// Do any additional setup after loading the view.
[self setViewControllers:[NSArray arrayWithObject:initialView]
direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];
}
The foaGiornataViewController is a view that doesn't exist on storyboard but is only a objective-c class.
Thanks in advances.
I have solved. I have setted opaque navigation bar for my Navigation Controller on storyboard.
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.