In iOS 5 I have a Storyboard with a modal view controller, that I would like to display if its the user's first time in the app, after that I would like to skip this view controller.
I set an NSDefault key to handle this but when I check to see if this is set and then use performSegueWithIdentifier to initiate the segue, nothing happens. If i put this segue behind a button it works fine...
I answered a similar question where the developer wanted to show a login screen at the start. I put together some sample code for him that can be downloaded here. The key to solving this problem is calling things at the right time if you want to display this new view controller, you will see in the example you have to use something like this
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"LoginViewController"];
[vc setModalPresentationStyle:UIModalPresentationFullScreen];
[self presentModalViewController:vc animated:YES];
}
I also have an explanation of how segues and storyboards work that you can see here
Loading in ViewDidLoad caused "under-layer" to flash. I solved this by loading my Storyboard programmatically. Thus, under Target/Main Storyboard - leave this blank. Then add the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Load Main App Screen
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
HomeScreenVC *homeScreenVC = [storyboard instantiateInitialViewController];
self.window.rootViewController = homeScreenVC;
[self.window makeKeyAndVisible];
// Load Login/Signup View Controller
UIViewController *mainLoginVC = [storyboard instantiateViewControllerWithIdentifier:#"MainLoginVC"];
[mainLoginVC setModalPresentationStyle:UIModalPresentationFullScreen];
[homeScreenVC presentModalViewController:mainLoginVC animated:NO];
return YES;
}
The problem is you are adding a second view to the hierarchy before the first is fully added. Try putting your code in:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Present your modal from here
}
After [super viewDidAppear] is called you have a fully loaded view to modify.
There is no principal problem with performing segues in viewDidLoad (after the call to super of course).
The problem is performing segues before the window of the application is made visible.
The UIViewController you want to display is part of the main storyboard so it is loaded into memory before the app begins running it's code in the app delegate. In your case, the viewDidLoad is called by iOS before your application window got message: MakeKeyAndVisible.
The important part is the visibility.
Performing a segue on a view hierarchy in which the window is not visible does nothing!
You can try to do something like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// The window initialized with hidden = YES, so in order to perform the segue we need to set this value to NO.
// After this action, the OS will add the window.rootViewController's view as a subview of the window.
self.window.hidden = NO;
[self.window.rootViewController performSegueWithIdentifier:_IDENTIFIER_ sender:self.window.rootViewController];
// Now that the window is not hidden, we must make it key.
[self.window makeKeyWindow];
return YES;
}
UPDATE: this solution no longer works in iOS 8.
A correct way to solve your problem is to trigger the segue / present modal view controller in applicationDidBecomeActive: app delegate method or in a UIApplicationDidBecomeActiveNotification notification handler.
Apple's documentation actually advises the same:
If your app was previously in the background, you could also use it to refresh your app’s user interface.
This solution has the advantage that it works with Main storyboard loading mechanism so that you don't need to load anything manually and write unnecessary code.
I use this solution successfully on iOS 6.1, 7.0 and 7.1 and it should work on iOS 5 either.
For Swift:
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("toView2", sender: self)
}
For Swift 3:
DispatchQueue.main.async {
self.performSegueWithIdentifier("toView2", sender: self)
}
This is how I did it in SWIFT. This also hides the View Controller.
override func viewWillAppear(animated: Bool) {
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
let isloggedIn = prefs.objectForKey("isLoggedIn") as? Bool
if (isloggedIn != false) {
self.view.hidden = true
} else {
self.view.hidden = false
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
let isloggedIn = prefs.objectForKey("isLoggedIn") as? Bool
if (isloggedIn != false) {
println("this should work")
self.performSegueWithIdentifier("Login", sender: self)
}
}
Swift 3
override func viewWillAppear(_ animated: Bool) {
if authPreference.isExist() == true {
self.view.isHidden = true
} else {
self.view.isHidden = false
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if authPreference.isExist() == true {
navigateToSegue()
}
}
I had the same problem. Before finding this question, I solved this issue by using async in the main thread. This way, this code will be called by the UI thread right after creating the view.
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:#"segueAlbums" sender:self];
});
This code can be called in the viewDidLoad method.
Updated for Swift 3
The code snippet below allows you to load whichever viewController you want. In my case it was a TabBarController if the user had a valid facebook login token. The benefit to this solution over the other Swift 3 solution is that it's instantaneous with no screen flicker.
func applicationDidBecomeActive(_ application: UIApplication) {
if FBSDKAccessToken.current() != nil {
self.window?.rootViewController?.present((self.window?.rootViewController?.storyboard?.instantiateViewController(withIdentifier: "TabBarController"))!, animated: false, completion: nil)
}
}
The best solution is to do this:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self performSegueWithIdentifier:#"NameSegue" sender:self];
}
I adapted #bearMountain answer for Swift 3.
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let yourInitialVC: UIViewController? = storyboard.instantiateViewController(withIdentifier: "TermsVC")
window?.rootViewController = termsVC
window?.makeKeyAndVisible()
return true
}
Related
I'm really struggling with this basic iOS programming stuff but I just can't figure out whats happening and how to solve it.
I have my main Login controller that detects when a user is logged in and presents next controller if auth succeed:
#interface LoginViewController (){
//Main root instance
RootViewController *mainPlatformRootControler;
}
-(void)loggedInActionWithToken:(NSString *)token anonymous:(BOOL)isAnon{
NSLog(#"User loged in.");
mainPlatformRootControler = [self.storyboard instantiateViewControllerWithIdentifier:#"rootViewCOntrollerStoryIdentifier"];
[self presentViewController:mainPlatformRootControler animated:YES completion:^{
}];
}
And that works well, no problem.
My trouble is handling logout. How do I delete completely the RootViewController instance and show a new one?
I can see that RootViewController instances are stacking cause I have multiple observers and after a logout and then login they are called multiple times (as many times I exit and re-enter).
I've tried the following with no success:
First detecting logout in RootViewController and dismissing:
[self dismissViewControllerAnimated:YES completion:^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"shouldLogOut" object:nil];
}];
And then in LoginViewController:
-(void)shouldLogOut:(NSNotification *) not{
NSLog(#"No user signed in");
mainPlatformRootControler = NULL;
mainPlatformRootControler = nil;
}
So how can I handle this? I know its a basic memory handle stuff but I just don't know how?
First, you have to observe "shouldLogOut" in viewDidLoad should be like below:
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(shouldLogout:) name:#"shouldLogout" object:nil];
and after that in dismissViewControllerAnimated should be like below:
[self dismissViewControllerAnimated:true completion:^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"shouldLogOut" object:nil];
}];
you need to define shouldLogOut: selector in login view controller
-(void)shouldLogOut:(NSNotification *) not{
mainPlatformRootControler = nil;
}
Hope this will help you!
The problem is likely that you are never dismissing the RootViewController when logout has happened. By setting the property mainPlatformRootControler to nil, you are just relinquishing ownership of the object from the perspective of LoginViewController. That says nothing about anything else that also owns a reference to the object behind mainPlatformRootControler.
To fix this add a notification observer inside RootViewController for the logout notification, and when that's received, dismiss itself via dismiss(animated:completion)
Bonus You also don't need the property mainPlatformRootControler if all you are doing is saving it around to nil it out. By properly dismissing it (in the manner I wrote above), it will automatically be cleaned up, and thus don't need to worry about niling it out either. (Now if you have other reasons for keeping mainPlatformRootControler around, then don't delete it obviously).
Because login and logout is the one-time process, so after login, instead of presenting new controller just replace login controller with main controller.
Let's understand this:
You have main application delegate with window.
Code in didFinishLaunch:
if (loggedIn) {
self.window = yourMainController
} else {
self.window = loginController
}
Code in LoginController:
LoginController will have instance of AppDelegate, and after login, you have to change
appDelegate.window = mainController
Code in MainController:
MainController will have instance of AppDelegate, and after logout, you have to change
appDelegate.window = loginController
I hope this helps !!
Did you add Notification observer in viewDidLoad of your LoginViewController look like below
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(shouldLogOut:) name:#"shouldLogOut" object:nil];
I guess you missed this, then your login class can not receive notification after RootViewController dismissed.
As you have said there are multiple observer creates issue, then you must have to remove your observer when you don't need it.
In your RootViewController
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Add observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(shouldLogout:) name:#"shouldLogout" object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Remove observer by name
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"shouldLogout" object:nil];
}
So in this way you don't have to think about your RootViewController is in stack or it is loaded from fresh etc. Because actual problem is with your observer.
There are many correct ways to manage view hierarchies, but I'll share one way I have found to be simple and affective.
Basically, I swap out the primary UIWindow's rootViewController at log out/in. Additionally, I programmatically provide the rootViewController rather than letting #UIApplicationMain to load the initial view controller. The benefit of doing this is that during app launch, if the user is logged in, then the Login.storyboard never has to be loaded.
The show function can be configured to suite your style, but I like cross dissolve transitions as they are very simple.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var window: UIWindow? = {
let window = UIWindow()
window.makeKeyAndVisible()
return window
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Your own logic here
let isLoggedIn = false
if isLoggedIn {
show(MainViewController(), animated: false)
} else {
show(LoginViewController(), animated: false)
}
return true
}
}
class LoginViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .red
let logoutButton = UIButton()
logoutButton.setTitle("Log In", for: .normal)
logoutButton.addTarget(self, action: #selector(login), for: .touchUpInside)
view.addSubview(logoutButton)
logoutButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
logoutButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)]
)
self.view = view
}
#objc
func login() {
AppDelegate.shared.show(MainViewController())
}
}
class MainViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .blue
let logoutButton = UIButton()
logoutButton.setTitle("Log Out", for: .normal)
logoutButton.addTarget(self, action: #selector(logout), for: .touchUpInside)
view.addSubview(logoutButton)
logoutButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
logoutButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
]
)
self.view = view
}
#objc
func logout() {
AppDelegate.shared.show(LoginViewController())
}
}
extension AppDelegate {
static var shared: AppDelegate {
// swiftlint:disable force_cast
return UIApplication.shared.delegate as! AppDelegate
// swiftlint:enable force_cast
}
}
private let kTransitionSemaphore = DispatchSemaphore(value: 1)
extension AppDelegate {
/// Animates changing the `rootViewController` of the main application.
func show(_ viewController: UIViewController,
animated: Bool = true,
options: UIViewAnimationOptions = [.transitionCrossDissolve, .curveEaseInOut],
completion: (() -> Void)? = nil) {
guard let window = window else { return }
if animated == false {
window.rootViewController = viewController
return
}
DispatchQueue.global(qos: .userInitiated).async {
kTransitionSemaphore.wait()
DispatchQueue.main.async {
let duration = 0.35
let previousAreAnimationsEnabled = UIView.areAnimationsEnabled
UIView.setAnimationsEnabled(false)
UIView.transition(with: window, duration: duration, options: options, animations: {
self.window?.rootViewController = viewController
}, completion: { _ in
UIView.setAnimationsEnabled(previousAreAnimationsEnabled)
kTransitionSemaphore.signal()
completion?()
})
}
}
}
}
This code is a complete example, you can create a new project, clear out the "Main Interface" field, and then put this code in the app delegate.
The resulting transition:
Since you are dismissing the RootViewController and you nil the reference after logout but the instance is not released, the only other possibility is that something else is keeping a reference to the RootViewController. You probably have a retain cycle.
A retain cycle happens if two objects have a strong reference to each other. And because an object cannot be deallocated until all of its strong references are released, then you have a memory leak.
Examples of retain cycle include:
RootViewController *root = [[RootViewController alloc] init];
AnOtherViewController *another = [[AnOtherViewController alloc] init];
//The two instances reference each other
root.anotherInstance = another;
another.rootInstance = root;
Or
self.block = ^{
//self is captured strongly by the block
//and the block is captured strongly by the self instance
NSLog(#"%#", self);
};
The solution is to use a weak pointer for one of the references. Since a weak pointer is one that does not retain its target.
e.g.
#property(weak) RootViewController *anotherInstance;
And
_typeof(self) __weak weakSelf = self
self.block = ^{
_typeof(self) strongSelf = weakSelf
//self is captured strongly by the block
//and the block is captured strongly by the self instance
NSLog(#"%#", strongSelf);
};
So I have a segue from ViewController to SecondViewController. This segue is triggered by a UIButton in ViewController and the modally presents SecondViewController. The Segue's Identifier is set ("SegueIdentifier") and I am able to call the segue programatically from within my ViewController.
When I try to do the same in my AppDelegate, I get the error that the compiler can't find a segue with the Identifier I set.
let viewController = ViewController()
viewController.performSegueWithIdentifier("SegueIdentifier", sender: nil)
Again, I literally copied and pasted the performSegueWithIdentifier method call from the aforementioned method in ViewController in which I also call performSegueWithIdentifier for the same segue and it works.
Any ideas?
In my one project I am managing this situation like,
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
NSString *identifier;
BOOL isSaved = [[NSUserDefaults standardUserDefaults] boolForKey:#"loginSaved"];
if (isSaved)
{
identifier=#"home1";
}
else
{
identifier=#"dis1";
}
UIStoryboard * storyboardobj=[UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *screen = [storyboardobj instantiateViewControllerWithIdentifier:identifier];
[self.window setRootViewController:screen];
return YES;
}
It is in objective c and just for reference. If it can help you :)
Update :
In swift something like,
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
var identifier: String = String()
let isSaved = NSUserDefaults.standardUserDefaults().boolForKey("loginsaved")
if isSaved
{
identifier = "home1"
}
else{
identifier = "dis1"
}
let storyboardobj: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let screen: UIViewController = storyboardobj.instantiateViewControllerWithIdentifier(identifier)
self.window!.rootViewController = screen
// Override point for customization after application launch.
return true
}
Hope this will help :)
If you are not trying to show your initial view then do like this:
UIApplication.sharedApplication().keyWindow?.rootViewController.performSegueWithIdentifier("SegueIdentifier", sender: nil)
And also it depends that you segue is set to your RootViewController. Please check that also before you do this.
Segue connection has some info with it like Source Controller, Destination Controller. So while you are calling the performSegue method from ViewController class, it will work because that class has the info of this Segue connection.
While you are calling that same Segue method from App Delegate, it will not work. Because App Delegate class doesn't have the info or definition about that Segue object.
You should also check whether the calling object comes under Navigation Controller or not.
A storyboard segue has to be tied to the specific viewController that you are trying to segue from. So you have to have an instance of the current view controller on the screen. You may use the UIApplication.sharedApplication().keyWindow?.rootViewController
Method to get access to the rootViewController in the window, however if you presented a view controller above the rootViewController and you want to perform the segue on the presented view controller you have to access the rootViewController.presentedViewController. And since you may have multiple presentedViewControllers, what you do is this:
//get the root view controller
var currentViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
//loop over the presented view controllers and until you get the top view controller
while currentViewController.presentedViewController != nil{
currentViewController = currentViewController.presentedViewController
}
//And finally
currentViewController.performSegueWithIdentifier("identifier", sender: nil)
How can I present a view controller from my AppDelegate and have a Navigation bar added to that view with a back button to the previous view? I need to do this programmatically from my AppDelegate. Currently I can push a controller from there, but it doesn't act like a segue. It doesn't add a nav bar with a back button. Now I know I should be able to add one myself, but when I do it gets hidden. Currently I'm using pushViewController(), but I imagine that's not the best way to do it.
I had something that I think is similar, if not the same:
HIGH LEVEL VIEW
The general composition of my App (thus far, and specific to the issue at hand - note: details about classes provided for context, not required for resolution) is as follows:
UIViewController (ViewController.swift) embedded in a UINavigationController
Buttons on UIViewController segue to a view with a custom class:
ExistingLocationViewController - subclass of:
UITableViewController
One of the buttons (Add New Location) in the UINavigationController's Toolbar segues to view with another custom class:
NewLocationViewController - subclass of:
UIViewController
CLLocationManagerDelegate
UITextFieldDelegate
There are a number of other items here, but I believe the above is sufficient as the foundation for the issue at hand
RESOLUTION
In order to preserve the navigation-bar (and tool-bar) going both forward and back - I have the following code in my custom classes (note: the following is Swift-3 code, you may have to adjust for Swift-2):
override func viewDidLoad() {
super.viewDidLoad()
//...
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) // #=# not sure if this is needed
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
You could actually omit the last two lines in viewWillDisappear, or perhaps even omit the entire override function
The net result (for me) was as depicted below:
Hope that helps.
If you want add a NavigationController in appDelegate you can do it like this,in this way,your viewcontroller is load from storyboard
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let vc = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("vc") as! ViewController
let nav = UINavigationController(rootViewController: vc)
self.window?.rootViewController = nav
self.window?.backgroundColor = UIColor.whiteColor()
self.window?.makeKeyAndVisible()
return true
}
I am having problem in pushing ViewController from AppDelegate when user pressed the Notification
Below is my code, but this code Crashes because the navigationController is nil
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
var rootViewController = self.window!.rootViewController;
let customDetailsViewController = CustomDetailsViewController();
rootViewController?.navigationController!.pushViewController(customDetailsViewController, animated: true);
}
Any idea? Thank you!!
If your navigation controller is nil, then your rootViewController (the one with the arrow in your Storyboard) is not inside a NavigationController. Can you post a screenshot of the relevant part of your Storyboard?
EDIT:
As you are using RESideController, you'll have a RootViewController not connected to anything in your Storyboard. That RootViewController conforms to ``protocol and you have some code like:
#implementation FASRootViewController
- (void)awakeFromNib
{
self.menuPreferredStatusBarStyle = UIStatusBarStyleLightContent;
self.scaleContentView = NO;
self.scaleMenuView = NO;
self.panGestureEnabled = YES;
self.contentViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"contentViewController"];
self.rightMenuViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"rightMenuViewController"];
self.delegate = (id<RESideMenuDelegate>)self.rightMenuViewController;
}
To "launch" your View controllers your need to add a identifier in the Storyboard
Also, this View Controller should be a UINavigationController (not your first "content" view controller)
I have this app I am working on and I need ALL my view controllers but one to be in portrait.
The single one view controller that is special I need it to be able to rotate to whatever orientation the phone is in.
To do that I present it modally (not embedded in a NavigationController)
So (for example) my structure is like this:
window - Portrait
root view controller (UINavigationController - Portrait)
home view controller (UIViewController - Portrait)
details view controller (UIViewController - Portrait)
.
.
.
modal view controller (UIVIewController - All)
Now when ever I dismiss my modal view controller in a landscape position my parent view controller is ALSO rotated even though it doesn't support that orientation.
All UIViewControllers and UINavigaionControllers in the app inherit from the same general classes which have these methods implemented:
override func supportedInterfaceOrientations() -> Int
{
return Int(UIInterfaceOrientationMask.Portrait.toRaw())
}
My modal view controller overrides this method once again and it looks like this:
override func supportedInterfaceOrientations() -> Int
{
return Int(UIInterfaceOrientationMask.All.toRaw())
}
Update 1
It looks like this is happening only on iOS8 Beta.
Does someone know if there is something that changed regarding view controller's rotation or is this just a bug in the beta?
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if ([self.window.rootViewController.presentedViewController isKindOfClass: [SecondViewController class]])
{
SecondViewController *secondController = (SecondViewController *) self.window.rootViewController.presentedViewController;
if (secondController.isPresented)
return UIInterfaceOrientationMaskAll;
else return UIInterfaceOrientationMaskPortrait;
}
else return UIInterfaceOrientationMaskPortrait;
}
And for Swift
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow) -> Int {
if self.window?.rootViewController?.presentedViewController? is SecondViewController {
let secondController = self.window!.rootViewController.presentedViewController as SecondViewController
if secondController.isPresented {
return Int(UIInterfaceOrientationMask.All.toRaw());
} else {
return Int(UIInterfaceOrientationMask.Portrait.toRaw());
}
} else {
return Int(UIInterfaceOrientationMask.Portrait.toRaw());
}
}
For more details check this link
I'm having the same issue with an app and after days of experimentation I came up with a solution which is not very nice but it works for now. I'm using the delegate method application:supportedInterfaceOrientationsForWindow: within the appdelegate.
I created a test project and put it here on github (including a GIF which shows the result...)
// note: it's not in swift but I hope it helps anyways
After much experimentation, I am convinced that this is a "feature" of iOS 8.
If you think about it, this makes perfect sense, because it has been coming for a long time.
In, say iOS 4, it was possible to force app rotation when changing view controllers in a tab bar controller and a navigation controller, as well as when presenting/dismissing a controller.
Then in iOS 6 it became impossible to force app rotation except when presenting/dismissing a view controller (as I explained in many answers, such as this one).
Now, in iOS 8, I conjecture that it will be impossible to force app rotation at all (except at launch). It can prefer a certain orientation, so that once it is in that orientation it will stay there, but it cannot force the app to go into that orientation.
Instead, your view controller is expected to "adapt". There are several WWDC 2014 videos concentrating on "adaptation", and now I'm starting to understand that this is one reason why this is so important.
EDIT: In seed 4, it looks like this feature (forcing rotation on presentation and dismissal) is returning!
We have an app deployed that has a landscape controller which presents a portrait-only view controller. Was reviewed by Apple on iOS 8. We are only overriding supportedInterfaceOrientations.
It's worth noting that we found lots of differences between betas 3,4,and 5. In the end we had to wait for the GM before making a concerted effort to update our app for iOS 8.
You need to be very careful in iOS 8 to make sure that you don't do this:
[self dismissViewControllerAnimated:YES completion:nil]
[self presentViewController:vc animated:YES completion:nil]
If the outgoing vc is portrait, and the incoming one is landscape, some view frames can end up very messed up. Present the incoming vc in the completion block of the dismiss call instead.
[self dismissViewControllerAnimated:YES completion:^{
[self presentViewController:vc animated:YES completion:nil]
}];
This is actually easier and can be done without any additional properties (here's an example with AVPlayerViewController):
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
if ([self.window.rootViewController.presentedViewController isKindOfClass: [AVPlayerViewController class]])
return self.window.rootViewController.presentedViewController.isBeingDismissed ?
UIInterfaceOrientationMaskPortrait : UIInterfaceOrientationMaskAll;
else
return UIInterfaceOrientationMaskPortrait;
} else {
return UIInterfaceOrientationMaskAll;
}
}
Awesome question and awesome answer provided by #ZaEeM ZaFaR! Combining his answer with this led me to a great and more generic solution.
The drawback of the first answer is that you have to manage the variable isPresented in every view controller that allows rotations. Furthermore, you have to expand the check and cast in supportedInterfaceOrientationsForWindow for every vc that allows rotation.
The drawback of the second answer is that it doesn't work; it also rotates the presenting vc when dismissing the presented vc.
This solution allows rotation in all vc where you put canRotate(){} and doesn't rotate the presenting vc.
Swift 3:
In AppDelegate.swift:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController) {
if (rootViewController.responds(to: Selector(("canRotate")))) {
// Unlock landscape view orientations for this view controller if it is not currently being dismissed
if !rootViewController.isBeingDismissed{
return .allButUpsideDown
}
}
}
// Only allow portrait (standard behaviour)
return .portrait
}
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
if (rootViewController == nil) {
return nil
}
if (rootViewController.isKind(of: UITabBarController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
} else if (rootViewController.isKind(of: UINavigationController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
} else if (rootViewController.presentedViewController != nil) {
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController
}
In each view controller where rotation should be allowed:
func canRotate(){}
In the root view controller, try adding:
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
Worked for me.
Swift 3.0 OR Above,
Just check "isBeingDismissed" property of presented view controller.
Below is sample code, This is will rotate presenting view controller to portrait mode immediately after presented view controller is dismissed.
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController)
{
if rootViewController.canRotateVC == true
{
if baseVC.isBeingDismissed == false
{
return .allButUpsideDown
}
}
}
return .portrait}
you can get topController by below code:
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController?{
if (rootViewController == nil) { return nil }if (rootViewController.isKind(of: (UITabBarController).self))
{
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
}
else if (rootViewController.isKind(of:(UINavigationController).self))
{
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
}
else if (rootViewController.presentedViewController != nil)
{
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController }
I had the same issue, finally found a solution to open the modal view controller in another UIWindow, and it worked smoothly.
Stack -
iOS8 - prevent rotation on presenting viewController
For code:
https://github.com/OrenRosen/ModalInWindow