I use to contextually switch key UIWindows in my app to provide a bit cleaner flow – Welcome window => Main screen with items list <=> Item container with burger menu and stuff.
Example function follows:
- (void)updateKeyWindow:(UIWindow *)window withTransition:(WindowTransition)transition
{
UIWindow *originalWindow = _keyWindow;
_keyWindow = window;
window.alpha = 0;
[originalWindow resignKeyWindow];
[originalWindow resignFirstResponder];
originalWindow.userInteractionEnabled = NO;
[window makeKeyAndVisible];
window.transform = (transition == WindowTransitionFlyDown) ? CGAffineTransformMakeScale(1.02, 1.02) :
(transition == WindowTransitionFlyUp) ? CGAffineTransformMakeScale(.96, .96) :
CGAffineTransformIdentity;
// [UIView animateWithDuration:.24 animations:^{
window.alpha = 1;
originalWindow.alpha = 0;
window.transform = CGAffineTransformIdentity;
originalWindow.transform = (transition == WindowTransitionFlyDown) ? CGAffineTransformMakeScale(.96, .96) :
(transition == WindowTransitionFlyUp) ? CGAffineTransformMakeScale(1.02, 1.02) :
CGAffineTransformIdentity;
// } completion:^(BOOL b){
[originalWindow resignFirstResponder];
[originalWindow removeFromSuperview];
[originalWindow.rootViewController.view removeFromSuperview];
originalWindow.rootViewController = nil;
originalWindow = nil;
// }];
}
I use animations to provide nice transitions, but I've commented it out to test if it's not the cause of the issue I have.
The thing is, after dropping originalWindow from the hierarchy and quitting the block/function, the UIWindow is NOT being released and hangs somewhere in the space. I've tested this with child class by putting breakpoint inside overloaded -dealloc.
I've checked both UIApplication's -keyWindow and AppDelegate's -window, both having new UIWindow object assigned.
However after tapping anywhere on the screen, the -dealloc for the previous UIWindow is triggered with some -[UITouch dealloc] stuff in the call stack.
I find this behaviour completely weird, there must be something wrong within the UIKit, I'm not expecting there's anything wrong on my side with this approach.
Refer to the resignKeyWindow documentation:
Never call this method directly. The system calls this method and posts UIWindowDidResignKeyNotification to let the window know when it is no longer key...
Try removing that and seeing if it fixes the crash.
The problem is UIApplication's keyWindow is weak by definition. So you should have to connect your UIWindow's strongly on some object; like ApplicationDelegate or some singleton.
Related
I am recreating the UINavigationController Push animation using the new iOS 7 custom transitioning APIs.
I got the view pushing and popping fine using animationControllerForOperation
I added a edge gesture recogniser for the interactive pop gesture.
I used a UIPercentDrivenInteractiveTransition subclass and integrated code from WWDC 2013 218 - Custom Transitions Using View Controllers
It looks like it removes the fromViewController by mistake, but I don't know why.
The steps are:
Interactive pop starts
Finger is lifted after a short distance - red screenshot
The view animates back a short distance.
Red view is removed (I think) - black screenshot.
The full code is on GitHub, but here are 2 parts which I guess are important.
Gesture delegate
- (void)didSwipeBack:(UIScreenEdgePanGestureRecognizer *)edgePanGestureRecognizer {
if (state == UIGestureRecognizerStateBegan) {
self.isInteractive = YES;
[self.parentNavigationController popViewControllerAnimated:YES];
}
if (!self.isInteractive) return;
switch (state)
{
case UIGestureRecognizerStateChanged: {
// Calculate percentage ...
[self updateInteractiveTransition:percentagePanned];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded: {
if (state != UIGestureRecognizerStateCancelled &&
isPannedMoreThanHalfWay) {
[self finishInteractiveTransition];
} else {
[self cancelInteractiveTransition];
}
self.isInteractive = NO;
break;
}
}
}
UIViewControllerAnimatedTransitioning protocol
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
// Grab views ...
[[transitionContext containerView] addSubview:toViewController.view];
// Calculate initial and final frames
toViewController.view.frame = initalToViewControllerFrame;
fromViewController.view.frame = initialFromViewControllerFrame;
[UIView animateWithDuration:RSTransitionVendorAnimationDuration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
toViewController.view.frame = finalToViewControllerFrame;
fromViewController.view.frame = finalFromViewControllerFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
Anyone know why the screen is blank? Or Can anyone point me to some sample code. Apple don't appear have any sample code for interactive transitions using the percent driven interactions.
The first issue was a bug in the Apple sample code that I copied. The completeTransition method should have a more intelligent BOOL parameter like this:
[UIView animateWithDuration:RSTransitionVendorAnimationDuration delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
toViewController.view.frame = finalToViewControllerFrame;
fromViewController.view.frame = finalFromViewControllerFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
Thanks to #rounak for pointing me to the objc.io post.
This then presented another issue relating to the animation. The animation would stop, present a blank view and them carry on. This was almost defiantly a UIKit bug. The fix was to set the completionSpeed to 0.99 instead of 1.0. The default value is 1.0 so I guess that setting it to this doesn't do some side effect in their custom setter.
// self is a UIPercentDrivenInteractiveTransition
self.completionSpeed = 0.99;
I don't think you need a UIPercentDrivenInteractiveTransition subclass. I just create a new UIPercentDrivenInteractiveTransition object, hold a strong reference to it and return it in interactionControllerForAnimationController method.
This link for interactive transitions is quite helpful.
Okay I am kind of new to IOS development, but I am writing an application where I am using a timer class to time out the user if they idle too long on any particular scene in my storyboard and it bumps the user back to the original scene/view. I have a single story board that is made up of several scenes/views(not sure what the correct word here is), and each scene has its own view controller.
I accomplish the timeout via the appdelegate class. See code below.
So I have the code working and it works great, but I am trying to make it so that it will ignore the timer if we are on the main scene.
I have googled this, read copious amounts of documentation, and have tried many things but so far I haven't been able to figure out how to get the currently viewed scene in the applicationDidTimeout method.
If I can get the name of the currently viewed scene/view, then I can choose to ignore the timer or not.
Does anyone know how to do this?
Thank you for your time.
#import "StoryboardAppDelegate.h"
#import "TIMERUIApplication.h"
#implementation StoryboardAppDelegate
#synthesize window = _window;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// applicaiton has timed out
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(applicationDidTimeout:) name:kApplicationDidTimeoutNotification object:nil];
return YES;
}
-(void)applicationDidTimeout:(NSNotification *) notif
{
NSLog (#"time exceeded!!");
UIViewController *controller = [[UIStoryboard storyboardWithName:#"Main" bundle:NULL] instantiateViewControllerWithIdentifier:#"StoryboardViewController"];
UINavigationController * navigation = [[UINavigationController alloc]initWithRootViewController:controller];
[self.window setRootViewController:navigation];
navigation.delegate = self;
navigation.navigationBarHidden = YES;
if (controller) {
#try {
[navigation pushViewController:controller animated:NO];
} #catch (NSException * ex) {
//“Pushing the same view controller instance more than once is not supported”
//NSInvalidArgumentException
NSLog(#"Exception: [%#]:%#",[ex class], ex );
NSLog(#"ex.name:'%#'", ex.name);
NSLog(#"ex.reason:'%#'", ex.reason);
//Full error includes class pointer address so only care if it starts with this error
NSRange range = [ex.reason rangeOfString:#"Pushing the same view controller instance more than once is not supported"];
if ([ex.name isEqualToString:#"NSInvalidArgumentException"] &&
range.location != NSNotFound) {
//view controller already exists in the stack - just pop back to it
[navigation popToViewController:controller animated:NO];
} else {
NSLog(#"ERROR:UNHANDLED EXCEPTION TYPE:%#", ex);
}
} #finally {
//NSLog(#"finally");
}
} else {
NSLog(#"ERROR:pushViewController: viewController is nil");
}
[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
}
#end
I'm assuming you've written the logic for the timer somewhere. Can you just invalidate the timer when you've popped back to the rootViewController?
Also instead of pushing a viewController onto the navigationViewController and handling the errors, you should check to see if the controller you're pushing is already in the stack like so:
if (![navigation.viewControllers containsObject:viewController] {
// push onto the stack
}
You could also check to see how many levels are currently in the navigationController by checking the count of the viewControllers array like so:
if ([navigation.viewControllers count] == 0) {
// I know we're on the main screen because no additional viewControllers have been added to the stack.
}
If you are not using modal controllers anywhere then the simplest solution would be
UINavigationController* nav = (UINavigationController*)self.window.rootViewController; // You could just save the nav as part of your app delegate
if (nav.viewControllers.count > 1){
[nav popToRootViewControllerAnimated:YES];
}
This is different then your current code because your main page will not be deleted and recreated every time the timer goes off
Okay I figured out how to do this. I was making this way too complicated.
To solve this I simply made a property and method in the app delegate class where I could set a scene name.
Then in each view controller header file I import the header file for the app delegate class and define a reference to it. Then in the load event for each view I simply set the scene name in the app delegate class using this line of code.
[myAppDelegate setSceneName:self.title];
Easy peasy!
So I'm trying to finish this iPhone app I started years ago. Anyway, I have an options menu, from which you can create a custom level (which goes to another view). Then when I return to the main menu, then the options menu, it just displays a black screen.
// shows options menu, works the first time
-(void) options : (id) sender{
[self.menuViewController.view removeFromSuperview];
[self.bg.view removeFromSuperview];
[self transition:self.view :navController.view];
}
// goes to custom built level
- (void) createCustom : (int) colorCount : (int) width : (int) height : (int) shuffles : (int) partners : (ToggleMode) toggleMode : (BOOL) colorblind{
self.gameViewController.isCustom = true;
self.gameViewController.width = width;
self.gameViewController.height = height;
self.gameViewController.shuffles = shuffles;
self.gameViewController.partners = partners;
self.gameViewController.toggleMode = toggleMode;
self.gameViewController.colorCount = colorCount;
srandom(arc4random());
[self.navController setNavigationBarHidden:true];
[self.gameViewController goToLevel];
[self transition:self.optionsMenu.customLevel.view : gameViewController.view];
}
- (void) transition : (UIView *) fromView : (UIView *) toView{
[UIView transitionFromView:fromView toView:toView duration:1.0 options:UIViewAnimationOptionTransitionFlipFromRight completion:NULL];
}
When I print out the objects involved, they all seem to still be there, so I have NO idea why it's just showing black. Please help me finish this app!
Why not display your options menu using a ModalViewController? It will display overtop your existing ViewController and will not dismiss/dealloc it. This will also remove the need to manually remove the views and view controller before transition, which is probably what is causing your black screen issue. You can add a button on this modal to dismiss it after the user has finished looking through the options.
Also as a general point on objective c, to make your life easier you should follow the correct format for naming methods. This involves putting the name of the parameter before each parameter in order to make calling the method much less confusing. As an example:
- (void)createCustom:(int)colorCount
:(int)width
:(int)height
:(int)shuffles
:(int)partners
:(ToggleMode)toggleMode
:(BOOL)colorblind {
}
Would turn into something like:
- (void)createCustomColorCount:(int)colorCount
withWidth:(int)width
withHeight:(int)height
withNumberOfShuffles:(int)shuffles
WithNumberOfPartners:(int)partners
withToggleMode:(ToggleMode)toggleMode
setColorBlind:(BOOL)colorblind {
}
To demonstrate why this is helpful, here is an example of how you would call your method:
[self createCustom:0
:100
:100
:5
:5
:nil
:NO];
versus:
[self createCustomColorCount:0
withWidth:100
withHeight:100
withNumberOfShuffles:5
WithNumberOfPartners:5
withToggleMode:nil
setColorBlind:NO];
As you can see with the second method it is much more clear what each parameter value corresponds to without having to go back to the implementation of the method.
I created an application and in ViewController.m , viewDidLoad , i wrote following code
for (UIView* v in self.view.subviews){
NSLog(#"View is %#",v);
NSLog(#"First Responder is %#",[v isFirstResponder]?#"YES":#"NO");
}
NSLog(#"First Responder is %#",[self isFirstResponder]?#"YES":#"NO");
NSLog(#"First Responder is %#",[self.view isFirstResponder]?#"YES":#"NO");
But it it returns NO for everything. What is my first responder by default ?
In your main view controller, do this:
for (UIView *sub in self.view.subviews)
{
if (sub.isFirstResponder)
NSLog(#"It's me!");
}
From documentation for OS X:
Determining First-Responder Status
Usually an NSResponder object can always determine if it's currently the first responder by asking its window (or itself, if it's an NSWindow object) for the first responder and then comparing itself to that object. You ask an NSWindow object for the first responder by sending it a firstResponder message. For an NSView object, this comparison would look like the following bit of code:
if ([[self window] firstResponder] == self) {
// do something based upon first-responder status
}
Note: This is for OS X. Unfortunately iOS doesn't have a similar system, but can be achieved via:
UIWindow* keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView* firstResponder = [keyWindow performSelector:#selector(firstResponder)];
This is undocumented, and could be rejected by Apple. For testing and exploratory research, this is handy for you.
Here are some documents on first responders in iOS:
Cocoa Application Competencies for iOS
Event Handling Guide for iOS
- (void)fadeOutSplash {
UIImageView *splash = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Default-Landscape~ipad.png"]];
[self.window.rootViewController.view addSubview:splash]; // <-- OBJECT IS BEING RETAINED HERE
[UIView animateWithDuration:0.5
animations:^{
splash.alpha = 0;
}
completion:^(BOOL finished) {
[splash removeFromSuperview];
}];
}
I think ARC is retaining my "splash" when I add it to the subview of the rootViewController. ARC should release "splash" when I run my animation completion because it removes my "splash" from it's own super view. However, I can see in the allocation instruments that this parent view controller is staying allocated and it shows the problem line being where splash is added to the rootViewController. What can I do to make sure "splash" is released?
I fixed this problem, but I'm not exactly sure how.. Here's the likely solution:
- (void)removeFromSuperView
{
// Use this space to manually release any non IB pointers / variables as needed
self.someDictionaryIMadeInInit = nil;
while(self.subviews.count > 0) [[self.subviews objectAtIndex:0] removeFromSuperView];
[super removeFromSuperView];
}
This a little trick I came up with for ARC related views. I recommend it more as a last resort because truly this should be solved the appropriate way, but it's worth a try to save you from tearing out your hair!