I've been trying to find some information regarding the new multitasking switcher in iOS 7 and especially the screenshot that the OS takes when the app is going into hibernation.
Is there any way to completely turn off this feature or screenshot? Or can I hide the app altogether from the switcher? The app needs to run in the background, but we do not want to show any screenshot from the app.
The screenshot is potentially a security-risk, think along the lines for banking-apps where your card number or account summary will be available to anyone that double-click on the home button on the device.
Anyone with any insight into this? Thanks.
In Preparing Your UI to Run in the Background, Apple says:
Prepare Your UI for the App Snapshot
At some point after your app enters the background and your delegate method returns, UIKit takes a snapshot of your app’s current user interface. The system displays the resulting image in the app switcher. It also displays the image temporarily when bringing your app back to the foreground.
Your app’s UI must not contain any sensitive user information, such as passwords or credit card numbers. If your interface contains such information, remove it from your views when entering the background. Also, dismiss alerts, temporary interfaces, and system view controllers that obscure your app’s content. The snapshot represents your app’s interface and should be recognizable to users. When your app returns to the foreground, you can restore data and views as appropriate.
See Technical Q&A QA1838: Preventing Sensitive Information From Appearing In The Task Switcher
In addition to obscuring/replacing sensitive information, you might also want to tell iOS 7 to not take the screen snapshot via ignoreSnapshotOnNextApplicationLaunch, whose documentation says:
If you feel that the snapshot cannot correctly reflect your app’s user interface when your app is relaunched, you can call ignoreSnapshotOnNextApplicationLaunch to prevent that snapshot image from being taken.
Having said that, it appears that the screen snapshot is still taken and I have therefore filed a bug report. But you should test further and see if using this setting helps.
If this was an enterprise app, you might also want to look into the appropriate setting of allowScreenShot outlined in the Restrictions Payload section of the Configuration Profile Reference.
Here is an implementation that achieves what I needed. You can present your own UIImageView, or your can use a delegate-protocol pattern to obscure the confidential information:
// SecureDelegate.h
#import <Foundation/Foundation.h>
#protocol SecureDelegate <NSObject>
- (void)hide:(id)object;
- (void)show:(id)object;
#end
I then gave my app delegate a property for that:
#property (weak, nonatomic) id<SecureDelegate> secureDelegate;
My view controller sets it:
- (void)viewDidLoad
{
[super viewDidLoad];
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
delegate.secureDelegate = self;
}
The view controller obviously implements that protocol:
- (void)hide:(id)object
{
self.passwordLabel.alpha = 0.0;
}
- (void)show:(id)object
{
self.passwordLabel.alpha = 1.0;
}
And, finally, my app delegate avails itself of this protocol and property:
- (void)applicationWillResignActive:(UIApplication *)application
{
[application ignoreSnapshotOnNextApplicationLaunch]; // this doesn't appear to work, whether called here or `didFinishLaunchingWithOptions`, but seems prudent to include it
[self.secureDelegate hide:#"applicationWillResignActive:"]; // you don't need to pass the "object", but it was useful during my testing...
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[self.secureDelegate show:#"applicationDidBecomeActive:"];
}
Note, I'm using applicationWillResignActive rather than the advised applicationDidEnterBackground, because, as others have pointed out, the latter is not called when double tapping on the home button while the app is running.
I wish I could use notifications to handle all of this, rather than the delegate-protocol pattern, but in my limited testing, the notifications aren't handled in a timely-enough manner, but the above pattern works fine.
This is the solution I worked with for my application:
As Tommy said: You can use the applicationWillResignActive. What I did was making a UIImageView with my SplashImage and add it as subview to my main window-
(void)applicationWillResignActive:(UIApplication *)application
{
imageView = [[UIImageView alloc]initWithFrame:[self.window frame]];
[imageView setImage:[UIImage imageNamed:#"Portrait(768x1024).png"]];
[self.window addSubview:imageView];
}
I used this method instead of applicationDidEnterBackground because applicationDidEnterBackground won't be triggered if you doubletap the home button, and applicationWillResignActive will be. I heard people say though it can be triggered in other cases aswell, so I'm still testing around to see if it gives problem, but none so far! ;)
Here to remove the imageview:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
if(imageView != nil) {
[imageView removeFromSuperview];
imageView = nil;
}
}
Hope this helps!
Sidenote: I tested this on both the simulator and a real device: It Won't Show on the simulator, but it does on a real device!
This quick and easy method will yield a black snapshot above your app's icon in the iOS7 or later app switcher.
First, take your app's key window (typically setup in AppDelegate.m in application:didFinishLaunchingWithOptions), and hide it when your app is about to move into the background:
- (void)applicationWillResignActive:(UIApplication *)application
{
if(isIOS7Or8)
{
self.window.hidden = YES;
}
}
Then, un-hide your app's key window when your app becomes active again:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
if(isIOS7Or8)
{
self.window.hidden = NO;
}
}
At this point, check out the app switcher and verify that you see a black snapshot above your app's icon. I've noticed that if you launch the app switcher immediately after moving your app into the background, there can be a delay of ~5 seconds where you'll see a snapshot of your app (the one you want to hide!), after which it transitions to an all-black snapshot. I'm not sure what's up with the delay; if anyone has any suggestions, please chime in.
If you want a color other than black in the switcher, you could do something like this by adding a subview with any background color you'd like:
- (void)applicationWillResignActive:(UIApplication *)application
{
if(isIOS7Or8)
{
UIView *colorView = [[[UIView alloc] initWithFrame:self.window.frame] autorelease];
colorView.tag = 9999;
colorView.backgroundColor = [UIColor purpleColor];
[self.window addSubview:colorView];
[self.window bringSubviewToFront:colorView];
}
}
Then, remove this color subview when your app becomes active again:
- (void)applicationDidBecomeActive:(UIApplication *)application
{
if(isIOS7Or8)
{
UIView *colorView = [self.window viewWithTag:9999];
[colorView removeFromSuperview];
}
}
I used the following solution:
when application is going to resign I get appWindow snapshot as a View and add blur to it. Then I add this view to app window
how to do this:
in appDelegate just before implementation add line:
static const int kNVSBlurViewTag = 198490;//or wherever number you like
add this methods:
- (void)nvs_blurPresentedView
{
if ([self.window viewWithTag:kNVSBlurViewTag]){
return;
}
[self.window addSubview:[self p_blurView]];
}
- (void)nvs_unblurPresentedView
{
[[self.window viewWithTag:kNVSBlurViewTag] removeFromSuperview];
}
#pragma mark - Private
- (UIView *)p_blurView
{
UIView *snapshot = [self.window snapshotViewAfterScreenUpdates:NO];
UIView *blurView = nil;
if ([UIVisualEffectView class]){
UIVisualEffectView *aView = [[UIVisualEffectView alloc]initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
blurView = aView;
blurView.frame = snapshot.bounds;
[snapshot addSubview:aView];
}
else {
UIToolbar *toolBar = [[UIToolbar alloc] initWithFrame:snapshot.bounds];
toolBar.barStyle = UIBarStyleBlackTranslucent;
[snapshot addSubview:toolBar];
}
snapshot.tag = kNVSBlurViewTag;
return snapshot;
}
make your appDelegate implementation be the as follows:
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
//...
//your code
//...
[self nvs_blurPresentedView];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
//...
//your code
//...
[self nvs_unblurPresentedView];
}
I created Example projects in Swift and Objective C.
Both projects makes the following actions in:
-application:didResignActive - snapshot is created, blurred and added to app window
-application:willBecomeActive blur view is being removed from window.
How to use:
Objecitve C
Add AppDelegate+NVSBlurAppScreen .h and .m files to your project
in your -applicationWillResignActive: method add the following line:
[self nvs_blurPresentedView];
in your -applicationDidEnterBackground: method add the following line:
[self nvs_unblurPresentedView];
Swift
add AppDelegateExtention.swift file to your project
in your applicationWillResignActive function add the following line:
blurPresentedView()
in your applicationDidBecomeActive function add the following line:
unblurPresentedView()
if only use [self.window addSubview:imageView]; in applicationWillResignActive function, This imageView won't cover UIAlertView, UIActionSheet or MFMailComposeViewController...
Best solution is
- (void)applicationWillResignActive:(UIApplication *)application
{
UIWindow *mainWindow = [[[UIApplication sharedApplication] windows] lastObject];
[mainWindow addSubview:imageView];
}
Providing my own solution as an "answers", though this solution is very unreliable. Sometimes i get a black screen as the screenshot, sometimes the XIB and sometimes a screenshot from the app itself. Depending on device and/or if i run this in the simulator.
Please note i cannot provide any code for this solution since it's a lot of app-specific details in there. But this should explain the basic gist of my solution.
In AppDelegate.m under applicationWillResignActive i check if we're
running iOS7, if we do i load a new view which is empty with the
app-logo in the middle. Once applicationDidBecomeActive is called i
re-launch my old views, which will be reset - but that works for the
type of application i'm developing.
You can use activator to configure double clicking of home button to launch multitasking and disable default double clicking of home button and launching of multitasking window. This method can be used to change the screenshots to the application's default image. This is applicable to apps with default passcode protection feature.
Xamarin.iOS
Adapted from https://stackoverflow.com/a/20040270/7561
Instead of just showing a color I wanted to show my launch screen.
public override void DidEnterBackground(UIApplication application)
{
//to add the background image in place of 'active' image
var backgroundImage = new UIImageView();
backgroundImage.Tag = 1234;
backgroundImage.Image = UIImage.FromBundle("Background");
backgroundImage.Frame = this.window.Frame;
this.window.AddSubview(backgroundImage);
this.window.BringSubviewToFront(backgroundImage);
}
public override void WillEnterForeground(UIApplication application)
{
//remove 'background' image
var backgroundView = this.window.ViewWithTag(1234);
if(null != backgroundView)
backgroundView.RemoveFromSuperview();
}
Related
EDIT: It is appearing that there may not be ANY possible way to, on app termination only, set the device volume back to the level it was when an app started. To me this is a possible oversight on the part of Apple. Why wouldn't Apple want my app to be a good camper that leaves their campsite the way they found it, there must be a way, please help... I tried to get an answer to this broader question with another topic but it was closed as a duplicate, please go there and vote to re-open it :)
When my app terminates, I want to set the system volume back to the same volume it was when my app started (Edit: and not change the volume when the app enters the background).
I am attempting to do that with MPVolumeView as the mechanism to set the devices volume. From what I have researched, that seems to be the only way to set the devices volume programmatically. If there is another way, please suggest it.
So, when I launch my app I save the system volume as an external variable 'appStartVol' in my AppDelegate.m.
I let the user change the volume during app usage with MPVolumeView.
Then I try to set the system volume back to the appStartVol in the AppDelegate.m files' applicationWillTerminate. EDIT: applicationWillTerminate is called when a user dismisses apps from their recents list. I do this all the time and leave regularly used apps in recents so I don't have to scroll through 9 pages of icons to find them. So, there is a reason to do what I am asking, in that function.
I use this approach for screen brightness but can not seem to do it for volume.
I am having trouble because I do not seem to be able to get access to my storyboard MPVolumeView in AppDelegate.m applicationWillTerminate and I can not seem to make a local MPVolumeView work in AppDelegate.m applicationWillTerminate.
If I use a UIApplicationWillTerminateNotification notification in my view controller, I still run into the same problems in the notification event since the storyboard MPVolumeView also seems not accessible from that event.
EDIT: This is the reason that using code in applicationDidEnterBackground does not meet my needs: I want my users to be able to use my music player at the volume they have manually chosen in my app even when they decide to bring another app into focus. I believe that this is what the users would naturally assume would happen. For instance, why would the volume change if I want to use the calculator? I also want to believe that the natural assumption for a user would be that the volume should pop back to pre-app volume if the app is dismissed. Using applicationDidEnterBackground would make the app go to pre-app volume both when the app goes into background AND when it terminates, this is not acceptable.
ATTEMPT 1: Here is my code in my AppDelegate.m applicationWillTerminate:
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
NSLog(#"=== App is terminating ===");
UIScreen.mainScreen.brightness = brightnessORG;
NSLog(#"=== Screen brightness back to pre-app level of %f ===", brightnessORG);
UISlider *volumeViewSlider;
for (UIView *view in [_mpVolumeViewParentView subviews]){
if ([view.class.description isEqualToString:#"MPVolumeSlider"]){
volumeViewSlider = (UISlider*)view;
volumeViewSlider.value = appStartVol;
break;
}
}
}
ATTEMPT 1: Here is my AppDelegate.h:
#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>
extern CGFloat brightnessORG;
extern float appStartVol;
#interface AppDelegate : UIResponder <UIApplicationDelegate>
{
MPVolumeView *_mpVolumeViewParentView;
}
#property (strong, nonatomic) UIWindow *window;
#property (nonatomic, retain) IBOutlet MPVolumeView *mpVolumeViewParentView;
#end
Re. ATTEMPT 1: Although this code runs without error, it does not set the volume back to appStartVol since there are no views in the app delegates [_mpVolumeViewParentView subviews]. I am obviously not accessing the mpVolumeViewParentView that is in my storyboard.
ATTEMPT 2: Lets see if I can just add a local MPVolumeView in AppDelegate.m applicationWillTerminate:
- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
NSLog(#"=== App is terminating ===");
UIScreen.mainScreen.brightness = brightnessORG;
NSLog(#"=== Screen brightness back to pre-app level of %f ===", brightnessORG);
MPVolumeView *volumeView = [ [MPVolumeView alloc] init];
UISlider *volumeViewSlider;
volumeViewSlider = (UISlider*)volumeView;
volumeViewSlider.value = appStartVol;
}
Re. ATTEMPT 2: Runs with error = 'Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MPVolumeView setValue:]: unrecognized selector sent to instance'
But I have tried :), as you can see I am an objective-c newbie...
Any help would be appreciated :)
ATTEMPT 3: Try subviews in local MPVolumeView in applicationWillTerminate:
MPVolumeView *_mpVolumeViewParentView = [ [MPVolumeView alloc] init];
MPVolumeView *volumeView = [ [MPVolumeView alloc] init];
[_mpVolumeViewParentView addSubview:volumeView];
UISlider *volumeViewSlider;
for (UIView *view in [_mpVolumeViewParentView subviews]){
if ([view.class.description isEqualToString:#"MPVolumeSlider"]){
volumeViewSlider = (UISlider*)volumeView;
volumeViewSlider.value = appStartVol;
break;
}
}
Re. ATTEMPT 3: Runs with error at for loop initiation: 'Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MPVolumeView setValue:]: unrecognized selector sent to instance'
ATTEMPT 4: After adding AVfoundation framework to the project, I added this to my AppDelegate.m:
#import <AVFoundation/AVFoundation.h>
And I put these two lines to AppDelegate.m applicationWillTerminate:
AVAudioPlayer* wavplayer = [[AVAudioPlayer alloc] init];
wavplayer.volume = appStartVol;
Re. ATTEMPT 4: Runs with error at 'wavplayer.volume= appStartVol;': 'Thread 1: EXC_BAD_ACCESS (code=1, address=0x48)' , darn......
First, you are trying something that probably can't work - you are casting MPVolumeView to UISlider and then try to use UISlider's setValue: method. But your object is still MPVolumeView and support that method. So this can't work not because you are using it in wrong place, but in never works.
Also - appWillTerminate is not sufficient, as it's called only in one specific case. If app goes to background first, and then killed - the willTerminate is never called.
I assume you were trying to de something like described here iOS 9: How to change volume programmatically without showing system sound bar popup? - but you should just do the same - so find the UISlider within subviews, instead of casting the whole thing to it.
Edit:
#implementation AppDelegate
{
MPVolumeView* mv;
UISlider* slider;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
slider.value = 1;
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
mv = [MPVolumeView new];
for (UIView *view in [mv subviews]){
if ([view.class.description isEqualToString:#"MPVolumeSlider"]){
slider = ((UISlider*)view);
break;
}
}
}
#end
that's the sample code I used. Using in terminate unfortunately didn't work, so that's the most that I see that can be done (I also think it's the more correct way than using terminate, as it's not always called).
Remember this can stop working at any time, or even be rejected when submitted to the AppStore.
I have built an app like 2 years ago using story board. The root view controller of the app is UINavigation controller and the root view controller loads view controllers as per user interaction. From root view controller, depending on user action 8 other view controllers can be presented in UINavigation.
Now I would to include banner ads to that app.The problem is since I originally implemented the app using story board and viewcontrollers, I am wondering where to add the code for ad banner.
I did some research and it looks like the likely option is to
1. Add a UIViewcontroller as a root view controller
2. Add a ContainerView to the UIViewcontroller
3. Make Exiting navigation controller as embedded segue from the container view
4. Add the ad banner (Want the ad banner at the bottom of the screen).
In Other words
Existing
--> UINavigationController -->(segue)-->LoginController-->(segue)-->MainpageController-->
Planned Modification
--> UIViewController-->(has)-->Containerview-->(embed segue)-->UINavigationController -->(segue)-->LoginController-->(segue)-->MainpageController-->
What I like to know is, is this the best approach to implement ad banners in my case? or can I have ad banner view is each and every view controller which may be presented in navigation controller?
Thanks
Apple's BannerView sample app seems to cover this. It isn't a storyboard app but the basic rule should apply. There is a single VC that has the ad view as well as your app's view.
I wrote a blog post about it here: http://www.notthepainter.com/iad-admob-integration-with-a-dynamic-uiview/
Note, the blog post is about iAd and ad mob integration, but the same concepts should apply. You don't need to use ad integration to accomplish this at all. You can really ignore the rest of this answer and just see how Apple does it with the BannerView sample app.
Here's the blog posting:
’ve released 2 apps both with iAds. I used Apple’s BannerView sample code to implement this. Basically, in your delegate you don’t set root to your expected root UIViewController but rather you set root to a BannerView which contains your real root. When an iAd is available, your main view shrinks and the iAd is displayed at the bottom. When an ad isn’t available, your view expands to its “normal” size.
This worked very well in testing so I released both apps to the app store. However, when I first downloaded the versions from the store I was quite surprised to see no ads ever. It turns out that at least right now, iAd had a pretty horrible fill rate. So I wanted to show another ad when an iAd wasn’t available.
I found LARSAdController, an open source project by larsacus on GitHub. He makes ad integration very easy except for one thing. When you go down his quick development route you get the ads covering your view, it doesn’t shrink to accommodate the ad. This is a completely reasonable design decision, just not one I wanted.
So I decided to modify Apple’s BannerView to use LARSAdController. It was pretty easy.
The first thing you do is remove iAd from BannerView’s .h file and ad in the LARS TOLAdViewController class.
#import "TOLAdViewController.h"
#define KVO_AD_VISIBLE #"KVO_AD_VISIBLE"
#interface BannerViewController : TOLAdViewController
(Just ignore the KVO_AD_VISIBLE define for now, I’ll cover that later.) In the .m file also remove iAd and make these changes:
#implementation BannerViewController {
UIView *_bannerView;
UIViewController *_contentController;
BOOL isLoaded;
}
We changed _bannerView from an ADBannerView into a plain old UIVIew. ADBannerView also has a bannerLoaded property which we’ll have to replace with our isLoaded boolean. initWithContentViewController is also easy to modify.
// IAPHelper *sharedInstance = [//IAPHelper sharedInstance];
//if ([sharedInstance showBannerAds]) {
if (YES) {
_bannerView = [[UIView alloc] initWithFrame:CGRectZero];
} else {
_bannerView = nil; // not showing ads since the user has upgraded
}
Notice the commented out section. If you are using in-app purchases to transform an ad supported version into an ad free version you can do that right there.
At the end of the method well use Key-Value-Observing (KVO) to watch LARS and see when an ad is served or removed. (I’ll probably cover KVO in a future blog entry.)
[[LARSAdController sharedManager] addObserver:self
forKeyPath:kLARSAdObserverKeyPathIsAdVisible
options:0
context:KVO_AD_VISIBLE]
And the observing code:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
{
if(context == KVO_AD_VISIBLE) {
NSNumber *isVisible = [object valueForKey:kLARSAdObserverKeyPathIsAdVisible];
if ([isVisible boolValue]) {
_bannerView.frame = [[LARSAdController sharedManager] containerView].frame;
isLoaded = YES;
} else {
isLoaded = NO;
}
}
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}
We save the frame of the new ad and also update the isLoaded variable. (Originally thought I would need to call setNeedsLayout and layoutIfNeeded but in practice I didn’t.) The mods to viewDidLayoutSubviews were also pretty straightforward. The only odd part was removing the references to ADBannerContentSizeIdentifierPortrait and ADBannerContentSizeIdentifierLandscape and just replacing that all with a single line:
bannerFrame.size = [_bannerView sizeThatFits:contentFrame.size];
And a few lines later you use the new isLoaded variable
if (isLoaded) {
Don’t forget to clean up your observer in dealloc:
-(void) dealloc;
{
[[LARSAdController sharedManager] removeObserver:self forKeyPath:kLARSAdObserverKeyPathIsAdVisible];
}
All that remains is to tell LARS about your ads in your app delegate:
[[LARSAdController sharedManager] registerAdClass:[TOLAdAdapterGoogleAds class] withPublisherId:#"a14e55c99c24b43"];
[[LARSAdController sharedManager] registerAdClass:[TOLAdAdapteriAds class]];
LARSBannerViewController *root = [[LARSBannerViewController alloc] initWithNibName:#"LARSBannerViewController" bundle:nil];
_bannerViewController = [[BannerViewController alloc] initWithContentViewController:root];
[self.window setRootViewController:_bannerViewController];
And that’s it. Now your app should show iAD or AdMob ads and your view will shrink to accommodate them.
Of course there’s a bug, I don’t know if this is in AdMob server or in LARS but when you are in Landscape mode on an iPhone the ad’s size and the reported size are different leaving a black bar at the bottom of the screen. I’ve pinged larsacus about it and will update this post when I know more.
**Try This: **
let arrViews = NSMutableArray()
arrViews.add(self.viewEmail)
arrViews.add(self.viewEventTime)
arrViews.add(self.viewLocation)
for vc in arrViews
{
let vc1 = vc as! UIView
vc1.layer.masksToBounds = false
vc1.layer.shadowOffset = CGSize(width: 2.5, height: 2.5)
vc1.layer.shadowRadius = 5
vc1.layer.shadowOpacity = 0.5
}
I have an issue that I'm stuck on, but I have no idea why it even happens; If I push a detail controller on the stack, and I swipe back very quickly using the default left edge interactivePopGestureRecognizer, my parent/root view controller's UINavigationBar looks corrupt or something, almost like the built in iOS transition mechanism didn't have time to do it's job at resetting it after the detail view is gone. Also to clarify, everything in this 'corrupt' UINavigationBar is still touchable and everything on my parent/root view controller works perfectly.
For people downvoting due to no source code: there is no source code! This is an Apple bug!
Is there anyway to reset this UINavigationBar to what it should be when the parent/root view controller's viewDidAppear method gets called?
Note that this bug does not occur if I tap the top left back button instead of using the left edge interactivePopGestureRecognizer.
Edit: I added an NSLog to check the navigationBar's subview count on viewDidAppear on the parent/root view controller, and the count is always the same, corrupt or not, so I'd like to know why the popped controller is wreaking havoc with my UINavigationBar.
If you can help me at all, I'd greatly appreciate it! Thank you.
I've attached a screenshot of what it looks like: Note that the back chevron isn't part of my parent/root view controller, it's part of what was popped off the stack. Testing123 is the title for the parent/root view controller and not that of what was popped off the stack. The head and gear icons are part of the parent/root view controller.
Edit: I've thought something like this could fix the issue, but turns out it doesn't, and is really bad experience IMO too. This is not the kind of solution I'm looking for. I'm posting a large bounty so this can be resolved correctly! 😃. I just can't have this weird UI behavior be in a production quality app.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.navigationController pushViewController:[UIViewController new] animated:NO];
[self.navigationController popToRootViewControllerAnimated:YES];
}
TL;DR
I made a category on UIViewController that hopefully fixes this issue for you. I can't actually reproduce the navigation bar corruption on a device, but I can do it on the simulator pretty frequently, and this category solves the problem for me. Hopefully it also solves it for you on the device.
The Problem, and the Solution
I actually don't know exactly what causes this, but the navigation bar's subviews' layers' animations seem to either be executing twice or not fully completing or... something. Anyway, I found that you can simply add some animations to these subviews in order to force them back to where they should be (with the right opacity, color, etc). The trick is to use your view controller's transitionCoordinator object and hook into a couple of events – namely the event that happens when you lift your finger up and the interactive pop gesture recognizer finishes and the rest of the animation starts, and then the event that occurs when the non-interactive half of the animation finishes.
You can hook into these events using a couple methods on the transitionCoordinator, specifically notifyWhenInteractionEndsUsingBlock: and animateAlongsideTransition:completion:. In the former, we create copies of all of the current animations of the navbar's subviews' layers, modify them slightly, and save them so we can apply them later when the non-interactive portion of the animation finishes, which is in the completion block of the latter of those two methods.
Summary
Listen for when the interactive portion of the transition ends
Gather up the animations for all the views' layers in the navigation bar
Copy and modify these animations slightly (set fromValue to the same thing as the toValue, set duration to zero, and a few other things)
Listen for when the non-interactive portion of the transition ends
Apply the copied/modified animations back to the views' layers
Code
And here's the code for the UIViewController category:
#interface UIViewController (FixNavigationBarCorruption)
- (void)fixNavigationBarCorruption;
#end
#implementation UIViewController (FixNavigationBarCorruption)
/**
* Fixes a problem where the navigation bar sometimes becomes corrupt
* when transitioning using an interactive transition.
*
* Call this method in your view controller's viewWillAppear: method
*/
- (void)fixNavigationBarCorruption
{
// Get our transition coordinator
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
// If we have a transition coordinator and it was initially interactive when it started,
// we can attempt to fix the issue with the nav bar corruption.
if ([coordinator initiallyInteractive]) {
// Use a map table so we can map from each view to its animations
NSMapTable *mapTable = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory
valueOptions:NSMapTableStrongMemory
capacity:0];
// This gets run when your finger lifts up while dragging with the interactivePopGestureRecognizer
[coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Loop through our nav controller's nav bar's subviews
for (UIView *view in self.navigationController.navigationBar.subviews) {
NSArray *animationKeys = view.layer.animationKeys;
NSMutableArray *anims = [NSMutableArray array];
// Gather this view's animations
for (NSString *animationKey in animationKeys) {
CABasicAnimation *anim = (id)[view.layer animationForKey:animationKey];
// In case any other kind of animation somehow gets added to this view, don't bother with it
if ([anim isKindOfClass:[CABasicAnimation class]]) {
// Make a pseudo-hard copy of each animation.
// We have to make a copy because we cannot modify an existing animation.
CABasicAnimation *animCopy = [CABasicAnimation animationWithKeyPath:anim.keyPath];
// CABasicAnimation properties
// Make sure fromValue and toValue are the same, and that they are equal to the layer's final resting value
animCopy.fromValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.toValue = [view.layer valueForKeyPath:anim.keyPath];
animCopy.byValue = anim.byValue;
// CAPropertyAnimation properties
animCopy.additive = anim.additive;
animCopy.cumulative = anim.cumulative;
animCopy.valueFunction = anim.valueFunction;
// CAAnimation properties
animCopy.timingFunction = anim.timingFunction;
animCopy.delegate = anim.delegate;
animCopy.removedOnCompletion = anim.removedOnCompletion;
// CAMediaTiming properties
animCopy.speed = anim.speed;
animCopy.repeatCount = anim.repeatCount;
animCopy.repeatDuration = anim.repeatDuration;
animCopy.autoreverses = anim.autoreverses;
animCopy.fillMode = anim.fillMode;
// We want our new animations to be instantaneous, so set the duration to zero.
// Also set both the begin time and time offset to 0.
animCopy.duration = 0;
animCopy.beginTime = 0;
animCopy.timeOffset = 0;
[anims addObject:animCopy];
}
}
// Associate the gathered animations with each respective view
[mapTable setObject:anims forKey:view];
}
}];
// The completion block here gets run after the view controller transition animation completes (or fails)
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Iterate over the mapTable's keys (views)
for (UIView *view in mapTable.keyEnumerator) {
// Get the modified animations for this view that we made when the interactive portion of the transition finished
NSArray *anims = [mapTable objectForKey:view];
// ... and add them back to the view's layer
for (CABasicAnimation *anim in anims) {
[view.layer addAnimation:anim forKey:anim.keyPath];
}
}
}];
}
}
#end
And then just call this method in your view controller's viewWillAppear: method (in your test project's case, it would be the ViewController class):
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self fixNavigationBarCorruption];
}
After investigating this issue for some time with debug console, Instruments and Reveal, I have found out the following:
1) On simulator the bug can be recreated every time, if using Profile/Automation Template and adding the following script:
var target = UIATarget.localTarget();
var appWindow = target.frontMostApp().mainWindow();
appWindow.buttons()[0].tap();
target.delay(1);
target.flickFromTo({x:2, y: 100}, {x:160, y: 100});
2) On real device (iPhone 5s, iOS 7.1) this script never causes the bug. I tried various options for flick coordinates and the delay.
3) UINavigationBar consists of:
_UINavigationBarBackground (doesn't seem to be related to the bug)
_UIBackdropView
_UIBackgropEffectView
UIView
UIImageView
UINavigationItemView
UILabel (visible in the bug)
_UINavigationBarBackIndicatorView (visible in the bug)
4) When bug happens UILabel looks half transparent and in the wrong position, but the actual properties of the UILabel are correct (alpha: 1 and frame as in normal situation). Also _UINavigationBarBackIndicatorView looks doesn't correspond to actual properties - it is visible although it's alpha is 0.
From this I conclude that it's a bug of Simulator and that you can't even detect from the code that something is wrong.
So #troop231 - are you 100% sure this also happens on device?
Key Concept
Disable gesture recognizer when pushing view controller, and enable it when view appeared.
A Common Solution: Subclassing
You can subclass UINavigationController and UIViewController to prevent corruption.
MyNavigationController : UINavigationController
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[super pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO; // disable
}
MyViewController : UIViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES; // enable
}
Problem: Too annoying
Need to use MyNavigationController and MyViewController instead of UINavigationController and UIViewController.
Need to subclass for UITableViewController, UICollectionViewController, and more.
A Better Solution: Method Swizzling
It could be done by swizzling UINavigationController and UIViewController methods. Want to know about method swizzling, visit here.
Example below uses JRSwizzle that makes method swizzling easy.
UINavigationController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidLoad)
withMethod:#selector(hack_viewDidLoad)
error:nil];
[self jr_swizzleMethod:#selector(pushViewController:animated:)
withMethod:#selector(hack_pushViewController:animated:)
error:nil];
});
}
- (void)hack_viewDidLoad
{
[self hack_viewDidLoad];
self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
}
- (void)hack_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[self hack_pushViewController:viewController animated:animated];
self.interactivePopGestureRecognizer.enabled = NO;
}
UIViewController
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jr_swizzleMethod:#selector(viewDidAppear:)
withMethod:#selector(hack_viewDidAppear:)
error:nil];
});
}
- (void)hack_viewDidAppear:(BOOL)animated
{
[self hack_viewDidAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
Being Simple: Use Open Source
SwipeBack
SwipeBack does it automatically without any code.
With CocoaPods, just add a line below into your Podfile. You don't need to write any code. CocoaPods automatically import SwipeBack globally.
pod 'SwipeBack'
Install pod, and it's done!
Here's the scenario: I am using MWPhotoBrowser. I can push it fine once. The second time I try to push it the app crashes with nothing helpful. This is an ARC project, and this exact same code works fine in previous versions of the app. I am dumbfounded.
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
selectedIndexPath = indexPath;
if(browser == nil)
{
browser = [[MWPhotoBrowser alloc] initWithDelegate:self];
}
// Set options
browser.displayActionButton = YES; // Show action button to allow sharing, copying, etc (defaults to YES)
browser.displayNavArrows = YES; // Whether to display left and right nav arrows on toolbar (defaults to NO)
browser.zoomPhotosToFill = NO; // Images that almost fill the screen will be initially zoomed to fill (defaults to YES)
[browser setCurrentPhotoIndex:indexPath.row]; // Example: allows second image to be presented first
//browser.wantsFullScreenLayout = YES; // iOS 5 & 6 only: Decide if you want the photo browser full screen, i.e. whether the status bar is affected (defaults to YES)
// Present
[self.navigationController pushViewController:browser animated:YES];
}
Tap a cell once, MWPhotoBrowser opens fine. Go back, tap a cell again, the app crashes just with the debugger but no call stack or error reason:
(lldb):
browser is a strong member variable. It isn't being deallocated prematurely. I can also guarantee it is crashing exactly on the last line of the method.
Can someone please enlighten me on this? I'd solve it if the app at least gave me a reason for the crash and not just throw the debugger on my face.
I have used MWPhotoBrowser extensively and ran across the same problem once. The way I solved it was by checking the MWPhoto objects to see if they actually had been set properly. I realised that I had not created some of them, hence the crash. I would check the place where you store your MWPhotos and make sure they are all set. Hope this helps!
Having issues with the following code.
1.) What I'm trying to achieve; - To display a global background image or color in the AppDelegate of an iOS
2.) What is happening; - The image or color displayed suggests the code is working correctly as no errors or warnings are occurring.
3.) What is the issue; - The background color or image takes up the entire screen almost like it is the up most top layer therefore I see no elements i.e. uitext, buttons etc. Just the background image..good because it's working bad because, well...users can use the app!
4.) What I have done; - Below is the code snippet in my appdelegate that I have been working with. I know it works as it is in every UIViewController and I'm trying to refactor it so I only have it in one place. Not 200!!
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Set the background image
if ( IDIOM == IPAD )
{
self.window.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"background.jpg"]];
}
else
{
//Add gradient background
CAGradientLayer *bgLayer = [BackgroundLayer yellowGradient];
bgLayer.frame = self.window.bounds;
[self.window.layer insertSublayer:bgLayer atIndex:0];
}
//This is added so if in-app purchasing is used and the users connections fails in the middle of the transaction it will finalise the payment so it will download or give access to the purchase.
//[ReadingsIAPHelper sharedInstance];
return YES;
}
Any ideas?
Regards,
Jeremy
I solved the same issue with UIViewController categories that is in the common included file. So each UIviewController in its init function calls [self setMyColor].
This is my code:
#interface ColorViewController : UIViewController
- (void) setMyColor;
#end