How to animate dismissal of one view Controller to another using custom animation? - xcode4.5

I just figure out how to animate insertion of one viewControll
[self transitiontoViewController:self.filterController duration:.5 options:UIViewAnimationOptionTransitionNone animations:^{
[self.filterController.view resizeandMovewithRect:CGRectMake(0, self.filterController.view.frame.size.height, 0, 0)];
} completion:^(BOOL finished){
}];
Here, resizeAndMovewithRect simply move a view around using this category for UIView
-(void) resizeandMovewithRect:(CGRect)movement
{
CGRect myFrame = self.frame;
CGRect newFrame = CGRectMake(myFrame.origin.x+movement.origin.x, myFrame.origin.y + movement.origin.y, myFrame.size.width + movement.size.width , myFrame.size.height + movement.size.height);
self.frame =newFrame;
}
Now I want to do the opposite. I want self.filterController to be dismissed by moving it off screen.
One way to do so is:
[self transitiontoViewController:self.filterController duration:.5 options:UIViewAnimationOptionTransitionNone animations:^{
[self.filterController.view resizeandMovewithRect:CGRectMake(0, -self.filterController.view.frame.size.height, 0, 0)];
} completion:^(BOOL finished){
}];
If I do this the animation is not drawn at all.
It seems to me that self.filterController is removed from superview at the beginning of the animation. That's why whatever happen to self.filterController is not seen. So I do it this way
[UIView animateWithDuration:.5 animations:^{
[self.filterController.view resizeandMovewithRect:CGRectMake(0, -self.filterController.view.frame.size.height, 0, 0)];
} completion:^(BOOL finished){
[self transitiontoViewController:self.last2ViewsInTheContainer[0] duration:0 options:UIViewAnimationOptionTransitionNone animations:^{
} completion:nil];
}];
So I animate rolling up self.filterController.view first before I call [self transitiontoViewController
However, if I do it this way, then by the time the self.filterController.view is rolled up, the view that's replacing it is not added yet to superview. So that one is not seen.
How should I do it anyway?

I have found the answer. Turns out no views are removed till the end. The issue is that the new viewController is added to the top of the stack.
So when adding self.filterController, things are simple. It's already in front while the from view is in the back.
When pulling out self.filterController, that backView is in front hiding self.filterController. So the solution is to simply readd self.filterController to the superview and we'll see the animation.
Final code:
[self transitiontoViewController:self.last2ViewsInTheContainer[0] duration:.5 options:UIViewAnimationOptionTransitionNone animations:^{
[self.filterController.view.superview addSubview:self.filterController.view]; //Put the filterControllerView at the front
[self.filterController.view resizeandMovewithRect:CGRectMake(0, -self.filterController.view.frame.size.height, 0, 0)];
} completion:nil];

Related

UIView not resizing when rotated with a CGAffineTransform under iOS8

I have a UIViewController that only rotates some of it subviews when the device is rotated. This works fine under iOS7 but breaks under iOS8. It appears that the UIView's bounds are adjusted by the transform under iOS8. This was unexpected.
Here's some code:
#interface VVViewController ()
#property (weak, nonatomic) IBOutlet UIView *pinnedControls;
#property (nonatomic, strong) NSMutableArray *pinnedViews;
#end
#implementation VVViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.pinnedViews = [NSMutableArray array];
[self.pinnedViews addObject:self.pinnedControls];
}
-(void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[UIViewController rotatePinnedViews:self.pinnedViews forOrientation:self.interfaceOrientation];
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation) && UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
[UIViewController rotatePinnedViews:self.pinnedViews forOrientation:toInterfaceOrientation];
}
}
#end
We've made a category on UIViewController to handle this behavior. Here's the pertinent code:
#implementation UIViewController (VVSupport)
+ (void)rotatePinnedViews:(NSArray *)views forOrientation:(UIInterfaceOrientation)orientation {
const CGAffineTransform t1 = [UIViewController pinnedViewTansformForOrientation:orientation counter:YES];
const CGAffineTransform t2 = [UIViewController pinnedViewTansformForOrientation:orientation counter:NO];
[views enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
// Rotate the view controller
view.transform = t1;
[view.subviews enumerateObjectsUsingBlock:^(UIView *counterView, NSUInteger idx, BOOL *stop) {
// Counter-rotate the controlsUIin the view controller
counterView.transform = t2;
}];
}];
}
+ (CGAffineTransform)pinnedViewTansformForOrientation:(UIInterfaceOrientation)orientation counter:(BOOL)counter {
CGAffineTransform t;
switch ( orientation ) {
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationPortraitUpsideDown:
t = CGAffineTransformIdentity;
break;
case UIInterfaceOrientationLandscapeLeft:
t = CGAffineTransformMakeRotation(counter ? M_PI_2 : -M_PI_2);
break;
case UIInterfaceOrientationLandscapeRight:
t = CGAffineTransformMakeRotation(counter ? -M_PI_2 : M_PI_2);
break;
}
return t;
}
#end
Here's what the nib looks like:
The UIView named pinned in the nib is the IBOutlet of pinnedControls:
When I run this in portrait mode under iOS7 or iOS8 I get this:
And I see the desired outcome under iOS7 in landscape mode:
But under iOS8 (GM) I do not get this behavior. This is what I see instead:
Notice that the center of the UILabel with the text "Pinned Label" is maintaining its distance from the bottom of the pinned UIView, which has not changed size to accommodate the rotation. That UIView has all its edges pinned to the top, left, bottom and right sides of the super view.
It looks to me that the transform property interacts with Auto Layout differently under iOS8. I'm a bit baffled here. I know I can't rely on the frame. I may just start manually setting bounds but that just seems like the wrong thing to do, essentially do an end run around Auto Layout.
So this was driving me crazy for the past couple days and I was able to fix by changing the timing of the setTransform call in my animations block
When going to landscape, I'm setting the transform AFTER setting up the new frame. When going portrait, I'm setting the transform BEFORE setting up the new frame. All this was going inside the animations block on the "animateWithDuration..." method
I'm not sure if it will help you directly with your code, but it might spark some inspiration to solve it since we are definitely having a similar issue
This is more of a work around than a fix so it may not help everybody in similar situations. My problem was that the outer "pinned" view was being resized again after the transform was applied.
My solution was to change the constraints on the pinned view to be center vertically, center horizontally, and width and height equal a constant.
Then, in viewDidLoad, I set the height and width of the pinned view's frame to be the height of the main screen. This makes the view square so I don't care if it gets an extra rotate.
in ios8, uiviewcontrollers need to be resized with uitraitcollections depending on the device orientation. Otherwise, you get a uiview in portrait mode, while the phone oriented in landscape, when you try to rotate it. So the correct steps are to rotate AND override uitraitcollections
EDIT:
I override my uitraitcollection with the following code
UITraitCollection *horTrait = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
UITraitCollection *verTrait = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
UITraitCollection *finalTrait = [UITraitCollection traitCollectionWithTraitsFromCollections:#[horTrait,verTrait]];
[self.parentViewController setOverrideTraitCollection:finalTrait forChildViewController:self];
unfortunately, it doesnt work if the uiviewcontroller im trying to modify does NOT have a parentviewcontroller :'(

Navigation controller top layout guide not honored with custom transition

Short version:
I am having a problem with auto layout top layout guide when used in conjunction with custom transition and UINavigationController in iOS7. Specifically, the constraint between the top layout guide and the text view is not being honored. Has anyone encountered this issue?
Long version:
I have a scene which has unambiguously define constraints (i.e. top, bottom, left and right) that renders a view like so:
But when I use this with a custom transition on the navigation controller, the top constraint to the top layout guide seems off and it renders is as follows, as if the top layout guide was at the top of the screen, rather than at the bottom of the navigation controller:
It would appear that the "top layout guide" with the navigation controller is getting confused when employing the custom transition. The rest of the constraints are being applied correctly. And if I rotate the device and rotate it again, everything is suddenly rendered correctly, so it does not appear to be not a matter that the constraints are not defined properly. Likewise, when I turn off my custom transition, the views render correctly.
Having said that, _autolayoutTrace is reporting that the UILayoutGuide objects suffer from AMBIGUOUS LAYOUT, when I run:
(lldb) po [[UIWindow keyWindow] _autolayoutTrace]
But those layout guides are always reported as ambiguous whenever I look at them even though I've ensured that there are no missing constraints (I've done the customary selecting of view controller and choosing "Add missing constraints for view controller" or selecting all of the controls and doing the same for them).
In terms of how precisely I'm doing the transition, I've specified an object that conforms to UIViewControllerAnimatedTransitioning in the animationControllerForOperation method:
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController*)fromVC
toViewController:(UIViewController*)toVC
{
if (operation == UINavigationControllerOperationPush)
return [[PushAnimator alloc] init];
return nil;
}
And
#implementation PushAnimator
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
CGFloat width = fromViewController.view.frame.size.width;
toViewController.view.transform = CGAffineTransformMakeTranslation(width, 0);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.transform = CGAffineTransformMakeTranslation(-width / 2.0, 0);
toViewController.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
#end
I've also done a rendition of the above, setting the frame of the view rather than the transform, with the same result.
I've also tried manually make sure that the constraints are re-applied by calling layoutIfNeeded. I've also tried setNeedsUpdateConstraints, setNeedsLayout, etc.
Bottom line, has anyone successfully married custom transition of navigation controller with constraints that use top layout guide?
Managed to fix my issue by adding this line:
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
To:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext fromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC fromView:(UIView *)fromView toView:(UIView *)toView {
// Add the toView to the container
UIView* containerView = [transitionContext containerView];
[containerView addSubview:toView];
[containerView sendSubviewToBack:toView];
// animate
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
fromView.alpha = 0.0;
} completion:^(BOOL finished) {
if ([transitionContext transitionWasCancelled]) {
fromView.alpha = 1.0;
} else {
// reset from- view to its original state
[fromView removeFromSuperview];
fromView.alpha = 1.0;
}
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
From Apple's Documentation for [finalFrameForViewController] : https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerContextTransitioning_protocol/#//apple_ref/occ/intfm/UIViewControllerContextTransitioning/finalFrameForViewController:
I solved this by fixing the height constraint of the topLayoutGuide. Adjusting edgesForExtendedLayout wasn't an option for me, as I needed the destination view to underlap the navigation bar, but also to be able to layout subviews using topLayoutGuide.
Directly inspecting the constraints in play shows that iOS adds a height constraint to the topLayoutGuide with value equal to the height of the navigation bar of the navigation controller. Except, in iOS 7, using a custom animation transition leaves the constraint with a height of 0. They fixed this in iOS 8.
This is the solution I came up with to correct the constraint (it's in Swift but the equivalent should work in Obj-C). I've tested that it works on iOS 7 and 8.
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromView = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!.view
let destinationVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
destinationVC.view.frame = transitionContext.finalFrameForViewController(destinationVC)
let container = transitionContext.containerView()
container.addSubview(destinationVC.view)
// Custom transitions break topLayoutGuide in iOS 7, fix its constraint
if let navController = destinationVC.navigationController {
for constraint in destinationVC.view.constraints() as [NSLayoutConstraint] {
if constraint.firstItem === destinationVC.topLayoutGuide
&& constraint.firstAttribute == .Height
&& constraint.secondItem == nil
&& constraint.constant == 0 {
constraint.constant = navController.navigationBar.frame.height
}
}
}
// Perform your transition animation here ...
}
I struggled with the exact same problem. Putting this in the viewDidLoad of my toViewController really helped me out:
self.edgesForExtendedLayout = UIRectEdgeNone;
This did not solve all my issues and I'm still looking for a better approach, but this certainly made it a bit easier.
Just put the following code toviewDidLoad
self.extendedLayoutIncludesOpaqueBars = YES;
FYI, I ended up employing a variation of Alex's answer, programmatically changing the top layout guide's height constraint constant in the animateTransition method. I'm only posting this to share the Objective-C rendition (and eliminate the constant == 0 test).
CGFloat navigationBarHeight = toViewController.navigationController.navigationBar.frame.size.height;
for (NSLayoutConstraint *constraint in toViewController.view.constraints) {
if (constraint.firstItem == toViewController.topLayoutGuide
&& constraint.firstAttribute == NSLayoutAttributeHeight
&& constraint.secondItem == nil
&& constraint.constant < navigationBarHeight) {
constraint.constant += navigationBarHeight;
}
}
Thanks, Alex.
As #Rob mentioned, topLayoutGuide is not reliable when using custom transitions in UINavigationController. I worked around this by using my own layout guide. You can see the code in action in this demo project. Highlights:
A category for custom layout guides:
#implementation UIViewController (hp_layoutGuideFix)
- (BOOL)hp_usesTopLayoutGuideInConstraints
{
return NO;
}
- (id<UILayoutSupport>)hp_topLayoutGuide
{
id<UILayoutSupport> object = objc_getAssociatedObject(self, #selector(hp_topLayoutGuide));
return object ? : self.topLayoutGuide;
}
- (void)setHp_topLayoutGuide:(id<UILayoutSupport>)hp_topLayoutGuide
{
HPLayoutSupport *object = objc_getAssociatedObject(self, #selector(hp_topLayoutGuide));
if (object != nil && self.hp_usesTopLayoutGuideInConstraints)
{
[object removeFromSuperview];
}
HPLayoutSupport *layoutGuide = [[HPLayoutSupport alloc] initWithLength:hp_topLayoutGuide.length];
if (self.hp_usesTopLayoutGuideInConstraints)
{
[self.view addSubview:layoutGuide];
}
objc_setAssociatedObject(self, #selector(hp_topLayoutGuide), layoutGuide, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#end
HPLayoutSupport is the class that will act as a layout guide. It has to be a UIView subclass to avoid crashes (I wonder why this isn't part of the UILayoutSupport interface).
#implementation HPLayoutSupport {
CGFloat _length;
}
- (id)initWithLength:(CGFloat)length
{
self = [super init];
if (self)
{
self.translatesAutoresizingMaskIntoConstraints = NO;
self.userInteractionEnabled = NO;
_length = length;
}
return self;
}
- (CGSize)intrinsicContentSize
{
return CGSizeMake(1, _length);
}
- (CGFloat)length
{
return _length;
}
#end
The UINavigationControllerDelegate is the one responsible for "fixing" the layout guide before the transition:
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
toVC.hp_topLayoutGuide = fromVC.hp_topLayoutGuide;
id <UIViewControllerAnimatedTransitioning> animator;
// Initialise animator
return animator;
}
Finally, the UIViewController uses hp_topLayoutGuide instead of topLayoutGuide in the constraints, and indicates this by overriding hp_usesTopLayoutGuideInConstraints:
- (void)updateViewConstraints
{
[super updateViewConstraints];
id<UILayoutSupport> topLayoutGuide = self.hp_topLayoutGuide;
// Example constraint
NSDictionary *views = NSDictionaryOfVariableBindings(_imageView, _dateLabel, topLayoutGuide);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[topLayoutGuide][_imageView(240)]-8-[_dateLabel]" options:NSLayoutFormatAlignAllCenterX metrics:nil views:views];
[self.view addConstraints:constraints];
}
- (BOOL)hp_usesTopLayoutGuideInConstraints
{
return YES;
}
Hope it helps.
i found way. First uncheck "Extend Edges" property of controller. after that navigation bar getting dark color. Add a view to controller and set top and bottom LayoutConstraint -100. Then make view's clipsubview property no (for navigaionbar transculent effect). My english bad sory for that. :)
I had the same problem, ended up implementing my own topLayout guide view and making constraints to it rather then to topLayoutGuide. Not ideal. Only posting it here in case someone is stuck and looking for quick hacky solution http://www.github.com/stringcode86/SCTopLayoutGuide
Here's the simple solution I'm using that's working great for me: during the setup phase of - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext, manually set your "from" and "to" viewController.view.frame.origin.y = navigationController.navigationBar.frame.size.height. It'll make your auto layout views position themselves vertically as you expect.
Minus the pseudo-code (e.g. you probably have your own way of determining if a device is running iOS7), this is what my method looks like:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];
CGAffineTransform destinationTransform;
UIViewController *targetVC;
CGFloat adjustmentForIOS7AutoLayoutBug = 0.0f;
// We're doing a view controller POP
if(self.isViewControllerPop)
{
targetVC = fromViewController;
[container insertSubview:toViewController.view belowSubview:fromViewController.view];
// Only need this auto layout hack in iOS7; it's fixed in iOS8
if(_device_is_running_iOS7_)
{
adjustmentForIOS7AutoLayoutBug = toViewController.navigationController.navigationBar.frame.size.height;
[toViewController.view setFrameOriginY:adjustmentForIOS7AutoLayoutBug];
}
destinationTransform = CGAffineTransformMakeTranslation(fromViewController.view.bounds.size.width,adjustmentForIOS7AutoLayoutBug);
}
// We're doing a view controller PUSH
else
{
targetVC = toViewController;
[container addSubview:toViewController.view];
// Only need this auto layout hack in iOS7; it's fixed in iOS8
if(_device_is_running_iOS7_)
{
adjustmentForIOS7AutoLayoutBug = toViewController.navigationController.navigationBar.frame.size.height;
}
toViewController.view.transform = CGAffineTransformMakeTranslation(toViewController.view.bounds.size.width,adjustmentForIOS7AutoLayoutBug);
destinationTransform = CGAffineTransformMakeTranslation(0.0f,adjustmentForIOS7AutoLayoutBug);
}
[UIView animateWithDuration:_animation_duration_
delay:_animation_delay_if_you_need_one_
options:([transitionContext isInteractive] ? UIViewAnimationOptionCurveLinear : UIViewAnimationOptionCurveEaseOut)
animations:^(void)
{
targetVC.view.transform = destinationTransform;
}
completion:^(BOOL finished)
{
[transitionContext completeTransition:([transitionContext transitionWasCancelled] ? NO : YES)];
}];
}
A couple of bonus things about this example:
For view controller pushes, this custom transition slides the pushed toViewController.view on top of the unmoving fromViewController.view. For pops, fromViewController.view slides off to the right and reveals an unmoving toViewController.view under it. All in all, it's just a subtle twist on the stock iOS7+ view controller transition.
The [UIView animateWithDuration:...] completion block shows the correct way to handle completed & cancelled custom transitions. This tiny tidbit was a classic head-slap moment; hope it helps somebody else out there.
Lastly, I'd like to point out that as far as I can tell, this is an iOS7-only issue that has been fixed in iOS8: my custom view controller transition that is broken in iOS7 works just fine in iOS8 without modification. That being said, you should verify that this is what you're seeing too, and if so, only run the fix on devices running iOS7.x. As you can see in the code example above, the y-adjustment value is 0.0f unless the device is running iOS7.x.
I ran into this same issue but without using a UINavigationController and just positioning a view off of the topLayoutGuide. The layout would be correct when first displayed, a transition would take place to another view, and then upon exiting and returning to the first view, the layout would be broken as that topLayoutGuide would no longer be there.
I solved this problem by capturing the safe area insets prior to the transition and then reimplementing them, not by adjusting my constraints, but by setting them on the viewController's additionalSafeAreaInsets.
I found this solution to work well as I don't have to adjust any of my layout code and search through constraints and I can just reimplementing the space that was there previously. This could be more difficult if you are actually using the additionalSafeAreaInsets property.
Example
I added a variable to my transitionManager to capture the safe insets that exist when the transitionManager is created.
class MyTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting = true
private var container:UIView?
private var safeInsets:UIEdgeInsets?
...
Then during the entering transition I save those insets.
let toView = viewControllers.to.view
let fromView = viewControllers.from.view
if #available(iOS 11.0, *) {
safeInsets = toView.safeAreaInsets
}
In the case of the iPhone X this looks something like UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
Now when exiting, the insets on that same view we transitioned from in the entrance will be .zero so we add our captured insets to the additionalSafeAreaInsets on the viewController, which will set them on our view for us as well as update the layout. Once our animation is done, we reset the additionalSafeAreaInsets back to .zero.
if #available(iOS 11.0, *) {
if safeInsets != nil {
viewControllers.to.additionalSafeAreaInsets = safeInsets!
}
}
...then in the animation completion block
if #available(iOS 11.0, *) {
if self.safeInsets != nil {
viewControllers.to.additionalSafeAreaInsets = .zero
}
}
transitionContext.completeTransition(true)
try :
self.edgesforextendedlayout=UIRectEdgeNone
Or just set navigationbar opaque and set background image or backgroundcolor to navigationbar
In storyboard add another vertical constraint to main view's top. I have the same problem too but adding that constraint help me to avoid manual constraints. See screenshot here link
Other solution is to calculate toVC frame... something like this:
float y = toVC.navigationController.navigationBar.frame.origin.y + toVC.navigationController.navigationBar.frame.size.height;
toVC.view.frame = CGRectMake(0, y, toVC.view.frame.size.width, toVC.view.frame.size.height - y);
Let me know if you have found a better solution. I have been struggling with this issue as well and I came up with previous ideas.

How is this navigation control created

The second image on this page from Apple's user interface design guide show a segmented control inside of a tall navigation bar:
https://developer.apple.com/library/ios/documentation/userexperience/conceptual/mobilehig/Anatomy.html#//apple_ref/doc/uid/TP40006556-CH24-SW1
How has this been done? It seems to me that a UINavigationBar is always 64 pixels high, so I don't understand how they made this taller.
Is it a custom element (which would be surprising in this document), or is there an easy way to achieve this? I'm wondering if it's a UIToolbar... are they merged with the UINavigationBar under iOS 7? If so, how do we do this?
Note that I need to do this in a iPad app, where the UINavigationController is inside a split view controller.
I finally found the solution to this.
I had to override UINavigation bar with my custom subclass in order to change the height. By using the appearance proxy the title and navigation items can be repositioned correctly. Unfortunately the proxy can't be used to shift the back button's arrow up (on iOS 7), so we have to override layoutSubview to handle that.
#define kAppNavBarHeight 66.0
#implementation TATallNavigationBar
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self setupAppearance];
}
return self;
}
- (id)init
{
self = [super init];
if (self) {
[self setupAppearance];
}
return self;
}
- (void)setupAppearance {
static BOOL appearanceInitialised = NO;
if (!appearanceInitialised) {
// Update the appearance of this bar to shift the icons back up to their normal position
CGFloat offset = 44 - kAppNavBarHeight;
[[TATallNavigationBar appearance] setTitleVerticalPositionAdjustment:offset forBarMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearanceWhenContainedIn:[RRSNavigationBar class], nil] setBackgroundVerticalPositionAdjustment:offset forBarMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearanceWhenContainedIn:[RRSNavigationBar class], nil] setBackButtonBackgroundVerticalPositionAdjustment:offset forBarMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearanceWhenContainedIn:[RRSNavigationBar class], nil] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, offset) forBarMetrics:UIBarMetricsDefault];
appearanceInitialised = YES;
}
}
- (CGSize)sizeThatFits:(CGSize)size {
return CGSizeMake(self.superview.frame.size.width, kNavBarheight);
}
- (void)layoutSubviews {
static CGFloat yPosForArrow = -1;
[super layoutSubviews];
// There's no official way to reposition the back button's arrow under iOS 7. It doesn't shift with the title.
// We have to reposition it here instead.
for (UIView *view in self.subviews) {
// The arrow is a class of type _UINavigationBarBackIndicatorView. We're not calling any private methods, so I think
// this is fine for the AppStore...
if ([NSStringFromClass([view class]) isEqualToString:#"_UINavigationBarBackIndicatorView"]) {
CGRect frame = view.frame;
if (yPosForArrow < 0) {
// On the first layout we work out what the actual position should be by applying our offset to the default position.
yPosForArrow = frame.origin.y + (44 - kAppNavBarHeight);
}
// Update the frame.
frame.origin.y = yPosForArrow;
view.frame = frame;
}
}
}
#end
Note that it's easy to specify your subclass in XCode: clicking on the UINavigationController gives you access to the UINavigationBar in the left hand column. Click that and change it's subclass in the inspector.
I've also created a Gist for this:
https://gist.github.com/timothyarmes/7080170

After Taking Picture cannot select Use Photo or Retake

So I'm trying to update an app for iOS 7 and I'm running into issues with my custom overlay. The overlay is an image that I'm framing the photo with (both live and using a full resolution version to frame the final result in the camera roll). The problem is that now, under iOS 7, the overlay, while transparent at the bottom, provides access to the regular "take picture" button, but for some reason will not let me tap on the "Use Photo" or "Retake" buttons that come up after the picture is snapped. Here's the code snippet calling the view controller:
- (IBAction)takePhoto:(UIButton *)sender {
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.allowsEditing = NO;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.showsCameraControls = YES;
// Overlay Creation
UIView* overlayView = [[UIView alloc] initWithFrame:picker.view.frame];
overlayView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"PBOverlayView.png"]];
[overlayView.layer setOpaque:NO];
overlayView.opaque = NO;
picker.cameraOverlayView = overlayView;
[self presentViewController:picker animated:YES completion:NULL];
}
Another approach could be to observe the notifications when the ImagePicker changes state and remove (or disable) your overlay when you move into the "Use Photo" screen.
- (void) addPhotoObservers {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(removeCameraOverlay) name:#"_UIImagePickerControllerUserDidCaptureItem" object:nil ];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(addCameraOverlay) name:#"_UIImagePickerControllerUserDidRejectItem" object:nil ];
}
- (void) removePhotoObservers {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void)addCameraOverlay {
if (self.cameraPicker) {
self.cameraPicker.cameraOverlayView = self.myCameraOverlayView;
}
}
-(void)removeCameraOverlay {
if (self.cameraPicker) {
self.cameraPicker.cameraOverlayView = nil;
}
}
You could set User Enteraction Enabled to NO on Overlay View ;) works for me
Your problem: When you initialised the overlayView, you've set the frame to be the same size as that of the picker.
UIView* overlayView = [[UIView alloc] initWithFrame:picker.view.frame];
Explanation: Before capturing an image, the camera buttons are in the foreground of the overlayView, so that there's no issue pressing them.
After capturing an image (on the "retake/use preview page"), the overlayView is the one to be in the foreground, thus blocking the access to the buttons.
I know this to be an issue on iOS7, and not sure about other versions.
Possible solutions: Since this is a native problem within Apple's UIImagePickerController, I can only think of two solutions: (1) If possible, configure the overlayView to have a shorter frame which doesn't cover the bottom of the picker; (2) If you need the overlay to cover these buttons, you still have the exhausting option of configuring self.imagePicker.showsCameraControls = NO; but then you'd have to customize ALL of the camera behaviour (you can find many examples for that on the web).
After taking a photo, or animation on your overlay is done, i made it work by removing the overlay from its superview.
[UIView animateWithDuration:1
delay:1
options:UIViewAnimationOptionCurveEaseOut
animations:^{
} completion:^(BOOL finished) {
[self.view removeFromSuperview];
}];

UINavigationItem multiple line prompt text

Can any body give me the solution for displaying UINavigationItem prompt text in 2 lines?
There is not a built-in way to do this. Below is a work-around that seems to work pretty well that I put together from stackOverflow post UINavigationItem with prompt and activity indicator
Here is a simulator screen shot of what it creates:
Note that since the text is a UILabel you can modify its color, font, or anything else too.
// I have this code in viewDidLoad
UIView *viewContainingPrompt;
UIBarButtonItem *promptButtonItem;
// Configuring the prompt title of the navigation bar so it is present but empty
[self.navigationItem setPrompt: #""];
// We will create a UIBarButtonItem that has a custom view (viewContainingPrompt).
// A subview of viewContainingPrompt will be a UILabel (headerLabel)
// We need to have this "intermediate" view to position the label at the right position
// (the UIBarButtonItem ignores the origin and height of its custom view)
viewContainingPrompt = [[UIView alloc] initWithFrame: CGRectMake(0, 0, 0, 85)];
viewContainingPrompt.autoresizingMask = UIViewAutoresizingFlexibleWidth;
// Choose a width that puts 10 points on either end...
CGFloat labelWidth = self.navigationController.navigationBar.bounds.size.width - 20.0;
// Note that the '-60' below is determined by the width of the back button
// If someone can figure out how to determine this width at runtime this code
// would be much more robust.
UILabel *headerLabel = [[UILabel alloc] initWithFrame: CGRectMake(-60,-8,labelWidth,36)];
headerLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
headerLabel.text = #"A quite long prompt string that will wrap to a second line to demonstrate multiline prompt.";
headerLabel.font = [UIFont systemFontOfSize: 14];
headerLabel.numberOfLines = 0; // Zero gives as many lines as will fit, could be 2
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.textColor = [UIColor colorWithRed: .1 green: .1 blue: .2 alpha: 0.8f];
headerLabel.shadowColor = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 0.5f];
headerLabel.shadowOffset = CGSizeMake( 0, 1 );
headerLabel.textAlignment = UITextAlignmentCenter;
[viewContainingPrompt addSubview: headerLabel];
//[headerLabel release]; // Uncomment if not using ARC
promptButtonItem = [[UIBarButtonItem alloc] initWithCustomView: viewContainingPrompt];
self.navigationItem.leftBarButtonItem = promptButtonItem;
self.navigationItem.leftItemsSupplementBackButton = YES;
//[viewContainingPrompt release]; // Uncomment if not using ARC
//[promptButtonItem release]; // Uncomment if not using ARC
I would appreciate anyone's feedback on how to figure out the width of the back button during execution so that width did not have to be hard coded.
As it is I do not think there are any private APIs or other illegal code contained.

Resources