I just got a crash showing as the following screen shot, it happened when I click the back button on the navigation bar, is any typical situation will cause this crash?
In my experience there was an issue introduced in iOS 7 making it possible for you to start a transition before another has ended, which ultimately causes this crash. You can reproduce this manually if you put 2 navigation calls back to back and run them, such as:
[self.navigationController pushViewController:whatever animated:YES];
[self.navigationController pushViewController:whatever2 animated:YES];
If you do this, you will eventually see that crash occur.
The easiest way I found to make sure this never happens is by subclassing UINavigationController and implementing the UINavigationControllerDelegate to prevent overlapping transitions.
Once I began using the code below, the number of crashes I see due to this issue has dropped to 0.
One thing to note is that if you actually need to implement another <UINavigationControllerDelegate> you will need to write some code to store the extra delegate yourself and pass on the delegate calls, perhaps using NSProxy or something like that.
#interface MyNavigationController () <UINavigationControllerDelegate>
{
// used to prevent "can't add self as subview" crashes which occur when trying to animate 2 transitions simultaneously
BOOL _currentlyAnimating;
}
#end
#implementation MyNavigationController
- (void) viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
}
- (void) pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if(_currentlyAnimating)
{
return;
}
else if(animated)
{
_currentlyAnimating = YES;
}
[super pushViewController:viewController animated:animated];
}
- (UIViewController *) popViewControllerAnimated:(BOOL)animated
{
if(_currentlyAnimating)
{
return nil;
}
else if(animated)
{
_currentlyAnimating = YES;
}
return [super popViewControllerAnimated:animated];
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
_currentlyAnimating = NO;
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
// tracking cancelled interactive pop
// http://stackoverflow.com/questions/23484310/canceling-interactive-uinavigationcontroller-pop-gesture-does-not-call-uinavigat
[[self transitionCoordinator] notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
if([context isCancelled])
{
UIViewController *fromViewController = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
[self navigationController:navigationController willShowViewController:fromViewController animated:animated];
if([self respondsToSelector:#selector(navigationController:didShowViewController:animated:)])
{
NSTimeInterval animationCompletion = [context transitionDuration] * [context percentComplete];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (uint64_t)animationCompletion * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self navigationController:navigationController didShowViewController:fromViewController animated:animated];
});
}
}
}];
}
#end
Related
When a certain button is pressed in my app, the view should change orientation from portrait to
landscape. When the user comes back, the view controller should change back to portrait. But
sometimes the orientation doesn't change or the wrong view frame is used.
Here is my code
-(void)btnSignClicked:(CustomSignButton *)btn {
isSignButtonClicked = true;
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_0) {
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"];
}
else
{
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeRight animated:YES];
}
selectedWaiverId = btn.customTag;
SignatureView *obj = [[SignatureView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) delegate:self]; // Most of time got size (568,320) but some time i got (320,568), Don't know why
[self.view addSubview:obj];
}
#pragma mark - SIGNATUREVIEW DELEGATE
-(void)removeSignatureView:(SignatureView *)signatureView {
isSignButtonClicked = false;
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_0)
{
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationPortrait];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"]; // Some time not changed the orientation are view remaining in landscape
}
else
{
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait animated:YES];
}
[signatureView removeFromSuperview];
signatureView = nil;
}
#pragma mark
#pragma mark - Rotate screen
-(UIInterfaceOrientationMask)supportedInterfaceOrientations
{
if (isSignButtonClicked == true)
{
return UIInterfaceOrientationMaskLandscapeRight|UIInterfaceOrientationMaskLandscape;
}
else
{
return UIInterfaceOrientationMaskPortrait;
}
}
- (BOOL)shouldAutorotate
{
return YES;
}
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
if (isSignButtonClicked == true)
{
return (interfaceOrientation == UIInterfaceOrientationLandscapeRight);
}
else
{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
}
UPDATE
Sometimes viewWillTransitionToSize method is not called so I also integrate this notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
But sometimes this also does not work.
Add in AppDelegate.m file or any base controller file
#implementation UINavigationController (Orientation)
- (UIInterfaceOrientationMask) supportedInterfaceOrientations
{
return [(UIViewController*)[[self viewControllers] lastObject] supportedInterfaceOrientations];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return [(UIViewController*)[[self viewControllers] lastObject] preferredInterfaceOrientationForPresentation];
}
- (BOOL) shouldAutorotate
{
return [(UIViewController*)[[self viewControllers] lastObject] shouldAutorotate];
}
#end
Now put your ViewController object in UINavigationController object and push the view controller.
EX.
UINavigationController *obj=[[UINavigationController alloc] initWithRootViewController:_yourViewCtrlObj];
[self presentViewController:obj.....];
or
[self.navigationController pushViewController:obj animated:YES];
Set your desired orientation in all view controller.
If your app uses UINavigationViewController then create a custom class for UINAvigationController Like:
//CustomNavViewController.h
#import <UIKit/UIKit.h>
#interface CustomNavViewController : UINavigationController <UINavigationControllerDelegate>
#end
//CustomNavViewController.m
#import "CustomNavViewController.h"
#interface CustomNavViewController ()
#end
#implementation CustomNavViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (BOOL)shouldAutorotate {
return [self.visibleViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return [self.visibleViewController supportedInterfaceOrientations];
}
#end
And Now in your AppDelegate declare a property Like:
//AppDelegate.h
#property (assign, nonatomic) BOOL shouldRotate;
//AppDelegate.m
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if (self.shouldRotate) {
return UIInterfaceOrientationMaskLandscapeLeft|UIInterfaceOrientationMaskLandscapeRight;
}
return UIInterfaceOrientationMaskPortrait;
}
Now you can call orientation methods to ViewController which required fix orientation Like:
//YourViewController.m
-(BOOL)shouldAutorotate{
return NO;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskLandscape;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return UIInterfaceOrientationLandscapeLeft;
}
Now here is the trick set AppDelegate shouldRotate property to true and false for desired orientation
if you are using Default Presentation for ViewController then
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[appDelegate setShouldRotate:true];// Remember first update the value then present the ViewController
[self presentViewController:yourViewController animated:YES completion:nil];
Same as when you dismiss
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[appDelegate setShouldRotate:false];
[self dismissViewControllerAnimated:YES completion:nil];
If you are using storyBoards then add CustomNavViewController directly on Identity Inspector Custom class section
And after that follow above steps. Hope it's working
When you say "When the user comes back, the view controller should change back to portrait", do you mean that the user is hitting the back button on a navigation controller? If so, I saw this issue before and posted a solution that worked for me in another SO post: A: Locking device orientation on a view fails when going back in NavBar. I remember the transition being rough but it worked.
I also wrote a blog post a while back that looks at some other situations around view controller orientation locking.
See this link, in particular, I think you should check the conditions of your view controllers, that they meet Apple recommendations
e.g. check supportedInterfaceOrientations method of the top-most full-screen view controller
Try adding all your rotation changing code inside this block
dispatch_async(dispatch_get_main_queue(),
{
//changing orientation code + isSignButtonClicked = true (or false)
});
You need not use shouldAutorotate and shouldAutorotateToInterfaceOrientation.
As for the view frames getting wrong, you need to use autolayout constraints for each and every view you are using in this viewController even if you are creating views programmativally
I'm having trouble switching between two subclassed CollectionViewFlowLayouts.
I call the following method in my collectionViewController:
header:
#property (nonatomic, strong) PortraitFlowLayout *portraitFlowLayout;
#property (nonatomic, strong) LandscapeFlowLayout *landscapeFlowLayout;
Implementation:
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
if (UIDeviceOrientationIsPortrait(toInterfaceOrientation)) {
self.landscapeFlowLayout = nil;
[self portraitFlowLayout];
NSLog(#"Orientation portrait");
} else {
self.portraitFlowLayout = nil;
[self landscapeFlowLayout];
NSLog(#"Orientation landscape");
}
[self.collectionView.collectionViewLayout invalidateLayout];
}
and in the same collectionViewController:
- (LandscapeFlowLayout *)landscapeFlowLayout
{
if (_landscapeFlowLayout == nil) {
_landscapeFlowLayout = [[LandscapeFlowLayout alloc] init];
self.collectionView.collectionViewLayout = _landscapeFlowLayout;
}
[self.collectionView.collectionViewLayout invalidateLayout];
return _landscapeFlowLayout;
}
- (PortraitFlowLayout *)portraitFlowLayout
{
if (_portraitFlowLayout == nil) {
_portraitFlowLayout = [[PortraitFlowLayout alloc] init];
self.collectionView.collectionViewLayout = _portraitFlowLayout;
}
[self.collectionView.collectionViewLayout invalidateLayout];
return _portraitFlowLayout;
}
I know that both layout are valid, and working, since I'm pushing into this viewController form another viewCont, and I've tried to do it with both the landscape and the portrait layout, which works fine.
The problem arises when I change the orientation. The first orientation change is fine, and the layout change as it's supposed to. But when it's then rotated back (sometimes it will rotate back and forth a few times before crashing), it gives me the following error when I trace it with the Zombie template in Instruments:
How can I trace this error further? Or, fix the problem? Any help is greatly appreciated.
Thanks in advance
EDIT
The problem seems only to arise when rotating to portrait.
Chris
Try to use UIDeviceOrientationDidChangeNotification instead willRotateToInterfaceOrientation.
This lines from the Apple's docs:
To support an alternate landscape interface, you must do the following:
Implement two view controller objects. One to present a portrait-only interface, and the other to present a landscape-only interface.
Register for the UIDeviceOrientationDidChangeNotification notification. In your handler method, present or dismiss the alternate view controller based on the current device orientation.
- (void)awakeFromNib
{
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
- (void)orientationChanged:(NSNotification *)notification
{
//without selector event may be lost
[self performSelector:#selector(updateFrameWithOrientation) withObject:nil afterDelay:0];
}
-(void) updateFrameWithOrientation{
UIInterfaceOrientation deviceOrientation = [UIApplication sharedApplication].statusBarOrientation;
if (UIDeviceOrientationIsLandscape(deviceOrientation))
{
}
else if (UIDeviceOrientationIsPortrait(deviceOrientation))
{
}
[self.collectionView reloadData];
}
after I managed to play audio in background with MPMoviePlayerController and tried to make it receive remote controls. But when I click on the Play/Pause button there's no reaction and the audio keeps on playing.
Then I tried to show if there's a log-output but there's no output.
Here's my Code:
-(void)viewDidAppear:(BOOL)animated{
...
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
}
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
NSLog(#"remoteControlReceivedWithEvent: %#", event);
if (event.type==UIEventTypeRemoteControl) {
if (event.subtype==UIEventSubtypeRemoteControlPlay) {
NSLog(#"Play");
} else if (event.subtype==UIEventSubtypeRemoteControlPause) {
NSLog(#"Pause");
} else if (event.subtype==UIEventSubtypeRemoteControlTogglePlayPause) {
NSLog(#"Play Pause");
}
}
}
Thanks for your effort in advanced.
It looks like your view controller isn't in the responder chain.
That might be due to [self resignFirstResponder] in viewWillDisappear and/or another ViewController is in the front.
To make sure you receive these events you can implement a UIApplication subclass which will be able to receive the events always. So it doesn't matter which ViewController is visible.
The UIApplication is always in the end of the responder chain.
Create a subclass for UIApplication (MyUIApplication) and implements the following UIResponder methods:
- (void)remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
...
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
In main.m use the following to initiate the app (replace MyUIApplication and AppDelegate with your classes names):
#autoreleasepool {
return UIApplicationMain(argc, argv, NSStringFromClass([MyUIApplication class]), NSStringFromClass([AppDelegate class]));
}
I am using Xcode 4.5, targeting iOS5 and above.
I have a popover that allows a user to change the fonts of the underlying view.
When tapping on the font, the change does not occur, until after I close the popover and the underling view and reopen.
It is set up for delegation. And the receiving view does import the the FontsPopoverViewDelegate.
Any help toward a solution would be greatly appreciated.
The Delegate methods for fonts:
#protocol FontsPopoverViewDelegate <NSObject>
- (void)fontResize:(float)size forView:(int)type;
- (void)font:(int)fontID forView:(int)fView;
- (int)getFontForView:(int)fView;
- (float)getFontSizeForView:(int)fView;
#end
Methods implemented in underlying view:
- (void)fontResize:(float)size forView:(int)type {
fontSizes[type] = size;
[self invalidate];
}
- (void)font:(int)fontID forView:(int)fView {
fontIds[fView] = fontID;
[self invalidate];
}
- (int)getFontForView:(int)fView {
return fontIds[fView];
[self invalidate];
}
- (float)getFontSizeForView:(int)fView {
return fontSizes[fView];
[self invalidate]; // added to spark a reaction from the view
}
-(void) invalidate {
NSLog(#"Invalidate called");
[self saveTextChanges];
[self refreshBodyText];
[self refreshBackground];
[self refreshBodyText];
[self refreshDateFont];
[self refreshTitleFont];
}
Any help would be greatly appreciated.
I solved this by posting a notification from the font popover to the underlying view. In the underlying view, I added the call... [self viewWillAppear:YES];
Now, it is working perfectly.
I have a slider, within the Fonts popover, that changes the size and a notification and calling viewWillAppear within the notification works the same as the font change.
- (void)aFontChanged:(NSNotification *)notification
{
NSLog(#"aFontChanged notification?");
[self viewWillAppear:YES];
[self refreshBodyText];
[self refreshDateFont];
[self refreshTitleFont];
}
- (void)aFontSizeChanged:(NSNotification *)notification
{
NSLog(#"aFontSizeChanged notification?");
[self viewWillAppear:YES];
[self refreshBodyText];
[self refreshDateFont];
[self refreshTitleFont];
}
In my parent view controller I have this:
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"BECOMING FIRST RESPONDER");
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
}
- (void) viewWillDisappear:(BOOL)animated {
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
[super viewWillDisappear:animated];
}
In any child view controller that I want to handle remote control events I do this:
- (void) remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
if (receivedEvent.type == UIEventTypeRemoteControl) {
switch (receivedEvent.subtype) {
case UIEventSubtypeRemoteControlTogglePlayPause:
{
//do something
break;
}
default:
break;
}
}
}
The problem is that the events are received in one of the child classes but not the other, even though they are very similar. They are presented the same way. Neither of them override viewDidAppear so they should both become first responder when they appear and receive remote control events.
So my question is, what could be making the one view controller lose first responder status?
EDIT:
Some debugging logs show that the view controller actually is the first responder. But for some reason it's not receiving the events.
Try the following:
[self resignFirstResponder];