I have a presented view-controller that is supporting all interface orientations. However, the presenting view controller only should support portrait mode.
So far so good.
However, in iOS8, when I dismiss the view controller WHILE in landscape mode, landscape mode stays. And since I have shouldAutorotate set to NO it an never rotate back.
Question is, how can I force the presenting VC to return to portrait?
I currently have this workaround implemented:
- (BOOL)shouldAutorotate
{
if ([self interfaceOrientation] != UIInterfaceOrientationPortrait)
{
return YES;
}
else
{
return NO;
}
}
It allows to move the device into portrait and it will stay here, since after it's portrait autorotate is disable.
But it looks ugly until the user rotates his phone.
How to force it?
We had the exactly same problem.
You can rotate it programmatically by the code -
if ([UIApplication sharedApplication].statusBarOrientation != UIInterfaceOrientationPortrait) {
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationPortrait];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"];
}
There are 2 possible options -
1) before you dismiss the presented viewController, rotate to portrait if
needed
2) after you dismiss, rotate to portrait in the "viewDidAppear" of the presenting viewController.
One issue with this, is that you can't pass a completion block, but you can use the next callback in iOS8:
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
if (self.needToDismissAfterRotatation)
self.needToDismissAfterRotatation = NO;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// dismiss
}];
}
}
By the way, in iOS8 apple did a huge change in the way the screen rotates, when the app rotates, the screen rotates, and all the elements in the UIWindow rotates as well, this is why when the presented viewController rotates to landscape, also the presenting viewController rotates, even if it only supports portrait...
We struggled with this issue for many days, finally we came up with a solution to put the presented viewController in a new UIWindow, and this way it keeps the presenting viewController in portrait all the time
example project for that:
"modalViewController" in UIWindow
This code will force the UI back to portrait, assuming that the view controller you're trying to force portrait was already the root view controller (if it wasn't already the root, this code will restore the root view controller but not any other view controllers that have been pushed onto it):
UIInterfaceOrientation orientation = [[UIApplication
sharedApplication] statusBarOrientation];
if (orientation != UIInterfaceOrientationPortrait) {
// HACK: setting the root view controller to nil and back again "resets"
// the navigation bar to the correct orientation
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIViewController *vc = window.rootViewController;
window.rootViewController = nil;
window.rootViewController = vc;
}
It's not very pretty as it makes an abrupt jump after the top level view controller has been dismissed, but it's better than having it left in landscape.
Related
How my code is set up:
I use a navigation controller, with a delegate that controls the orientation:
class NavDelegate: UINavigationControllerDelegate {
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
print("Checking orientation")
if topViewController != nil {
return topViewController!.supportedInterfaceOrientations
}
return .portrait
}
...
}
The main view controller of my app is named MainController, and it's portrait-only:
class MainController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
...
}
I have another controller PhotoController, which supports all four orientations:
class PhotoController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .all
}
...
}
I push the PhotoController on top of the MainController, using pushViewController().
The problem:
Each controller by itself handles orientations correctly — MainController stays in portrait as I rotate the phone to all four orientations, and PhotoController rotates to all four orientations.
But this scenario doesn't work correctly:
Put the phone in landscape, with main controller on screen.
The controller is in portait (as expected).
Push the PhotoController on the stack.
I expect the PhotoController to open in landscape, based on the device orientation, but it remains in portrait. If I manually rotate the phone back and forth, the controller rotates with the phone. Obviously, the user should never have to do this, rotate the phone back and forth to get the controller to rotate.
When I tap the button in MainController that pushes PhotoController, navigationControllerSupportedInterfaceOrientations() isn't invoked.
What else I tried:
Overriding shouldAutorotate to return true in PhotoController.
... and in MainController.
Overriding supportedInterfaceOrientations in UINavigationController instead of using the delegate.
Checking UINavigationController.visibleViewController instead of .topViewController. This didn't work because the topviewController turns out to be an iOS private class UISnapshotModalViewController, not my class, so calling supportedInterfaceOrientation() on it presumably doesn't return the right value.
None of these work.
The solution turned out to be to invoke UIViewController. attemptRotationToDeviceOrientation() in viewWillAppear() or viewDidAppear().
In the former case, the animation of pushing the controller occurs concurrently with the device rotation animation, which looks odd. In the latter case, the rotation happens after the controller transition animation completes. Which also looks odd, though in a different way.
The Apple Music app has a nice transition when a view controller with a visible navigation bar ("My Music" view controller) has a view controller with a transparent navigation bar (An artist's view controller) pushed on to the stack.
I'm looking to recreate this transition myself.
I've set the navigationbar to be transparent with the following code:
private func _setNavigationBarVisible(isVisible isVisible: Bool)
{
title = nil
/*
Update the navigation bar's visibility.
Create a helper method to prevent running the same code twice.
*/
func _updateNavigationBarVisibility(isVisible isVisible: Bool, userViewController: UserViewController?)
{
// Create a dummy navigation bar we can rip the default values out of if it should be visible
let navigationBar = UINavigationBar()
let backgroundImage = isVisible ? navigationBar.backgroundImageForBarMetrics(.Default) : UIImage()
let shadowImage = isVisible ? navigationBar.shadowImage : UIImage()
userViewController?.navigationController?.navigationBar.setBackgroundImage(backgroundImage,
forBarMetrics: .Default)
userViewController?.navigationController?.navigationBar.shadowImage = shadowImage
}
// Animate alongside the view controller's presentation transition if there is one
let isTransitionAnimationRun = transitionCoordinator()?.animateAlongsideTransition({ [weak self] context in
_updateNavigationBarVisibility(isVisible: isVisible, userViewController: self)
}, completion: nil)
// Or just update the values if there's no transition
if isTransitionAnimationRun == false
{
_updateNavigationBarVisibility(isVisible: isVisible, userViewController: self)
}
}
This function is run in my viewWillAppear: and viewWillDisappear: methods passing false and true to the isVisible parameter respectively.
This code does achieve a similar effect, but despite the code doing what it should, I want a different effect.
Currently, this code animates the shadow image making them visible on both view controllers rather than like Apple Music where only the one that presented it should have the shadow. As for the background image, there's a visible "swipe" animation as the navigation bar's background animates from transparent to translucent.
There's also an issue with the swipe gesture which I am looking in to, but currently if during the swipe gesture you keep your finger in one position so no swiping occurs and then lift the finger, the navigationbar updates as though the swipe finished, but the view controllers become glitched and the stack is never pushed or popped despite using the fully useable navigation bar.
I have a UIViewController in my tvOS app which will only appear for a few seconds, and needs totally customizable MENU button handling. I create it like so:
- (void)viewDidLoad
{
[super viewDidLoad];
// Add a tap gesture recognizer to handle MENU presses.
UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
tapGestureRec.allowedPressTypes = #[#(UIPressTypeMenu)];
[self.view addGestureRecognizer:tapGestureRec];
}
- (void)handleTap:(UITapGestureRecognizer *)sender
{
// Code to process the MENU button is here.
}
I display the view controller using pushViewController:animated::
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:identifier];
[self pushViewController:controller animated:isAnimated];
I've found that if the user presses MENU as soon as the screen starts to appear, while the cross-fade transition effect is still displaying, they are able to dodge the UITapGestureRecognizer and go back to a previous screen, which is not intended. They can also cause issues by mashing MENU over and over again--eventually they will escape out of things that they should not be able to.
How can I ensure that the MENU press always reaches my override? Is there a way to specify an app-encompassing MENU button handler and still use a UINavigationController?
A solution that worked for me was to install a UITapGestureRecognizer onto self.navigationController.view. Any taps which get missed by the regular UIViewControllers end up getting caught by the navigation controller's recognizer instead. If you install UITapGestureRecognizers on each view controller you create, the only taps which will fall through the cracks to the navigation controller are taps that occur mid-transition, and it is safe to just ignore those taps completely.
Note that when you want MENU to go back to the home screen, you will need to temporarily remove this tap recognizer and let tvOS handle the tap on its own, as I could find no clean way to escape to the home screen in my code (short of exit(0)).
I have very similar problem because of this behaviour.
In my case I have custom focus management depending on MENU button click detected on pressesEnded and managed by preferredFocusedView. But when navigation controller is poping ViewController and user at this time make second click on MENU button, then UINavigationController class detect pressesEnded and then call preferredFocusedView on my destination ViewController where is my management but pressesEnded will be not called, because was "stolen" by UINavigationController.
Solution for this problem in my case is to create "dummy" class that will be used by UINavigationController like this:
class MenuPhysicalHackNavigationController: UINavigationController {
override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
override func pressesEnded(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
override func pressesCancelled(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
override func pressesChanged(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
}
}
In our App we have a log-in ViewController A. On user log-in, a request navigate is automatically called to navigate to the next ViewController B. However when this is done we want to remove the log-in ViewController A from the stack so the user cannot "go back" to the log-in view but goes back the previous ViewController before the log-in instead.
We thought about removing the ViewController A from the stack when ViewController B is loaded, but is there a better way?
In the Android version of the App we've set history=no (if I recall correctly) and then it works.
Is there an similar way to achieve this in MonoTouch and MvvmCross?
I ended up with removing the unwanted viewcontroller from the navigation controller. In ViewDidDisappear() of my login ViewController I did the following:
public override void ViewDidDisappear (bool animated)
{
if (this.NavigationController != null) {
var controllers = this.NavigationController.ViewControllers;
var newcontrollers = new UIViewController[controllers.Length - 1];
int index = 0;
foreach (var item in controllers) {
if (item != this) {
newcontrollers [index] = item;
index++;
}
}
this.NavigationController.ViewControllers = newcontrollers;
}
base.ViewDidDisappear(animated);
}
This way I way remove the unwanted ViewController when it is removed from the view. I am not fully convinced if it is the right way, but it is working rather good.
This is quite a common scenario... so much so that we've included two mechanisms inside MvvmCross to allow this....
a ClearTop parameter available in all ViewModel navigations.
a RequestRemoveBackStep() call in all ViewModels - although this is currently NOT IMPLEMENTED IN iOS - sorry.
If this isn't enough, then a third technique might be to use a custom presenter to help with your display logic.
To use : 1. a ClearTop parameter available in all ViewModel navigations.
To use this, simply include the ClearTop flag when navigating.
This is a boolean flag - so to use it just change:
this.RequestNavigate<ChildViewModel>(new {arg1 = val1});
to
this.RequestNavigate<ChildViewModel>(new {arg1 = val1}, true);
For a standard simple navigation controller presenter, this will end up calling ClearBackStack before your new view is shown:
public override void ClearBackStack()
{
if (_masterNavigationController == null)
return;
_masterNavigationController.PopToRootViewController (true);
_masterNavigationController = null;
}
from https://github.com/slodge/MvvmCross/blob/vnext/Cirrious/Cirrious.MvvmCross.Touch/Views/Presenters/MvxTouchViewPresenter.cs
If you are not using a standard navigation controller - e.g. if you had a tabbed, modal, popup or split view display then you will need to implement your own presentation logic to handle this.
You can't: 2. RequestRemoveBackStep().
Sadly it proved a bit awkward to implement this at a generic level for iOS - so currently that method is:
public bool RequestRemoveBackStep()
{
#warning What to do with ios back stack?
// not supported on iOS really
return false;
}
from https://github.com/slodge/MvvmCross/blob/vnext/Cirrious/Cirrious.MvvmCross.Touch/Views/MvxTouchViewDispatcher.cs
Sorry! I've raised a bug against this - https://github.com/slodge/MvvmCross/issues/80
3. You can always... Custom ideas
If you need to implement something custom for your iOS app, the best way is to do this through some sort of custom Presenter logic.
There are many ways you could do this.
One example is:
for any View or ViewModel which needs to clear the previous view, you could decorate the View or ViewModel with a [Special] attribute
in Show in your custom Presenter in your app, you could watch for that attribute and do the special behaviour at that time
public override void Show(MvxShowViewModelRequest request)
{
if (request.ViewModelType.GetCustomAttributes(typeof(SpecialAttribute), true).Any())
{
// do custom behaviour here - e.g. pop current view controller
}
base.Show(request);
}
Obviously other mechanisms might be available - it's just C# and UIKit code at this stage
I don't know about mvvm but you can simply Pop the viewcontroller (AC A) without animation and then push the new viewcontoller (AC B) with animation
From within AC A:
NavigationController.PopViewControllerAnimated(false);
NavigationController.PushViewController(new ACb(), true);
I have a UIViewController which contains a new UIViewController like below,
#implementation ParentViewController
- (id)init
{
// some stuff
}
- (BOOL)CreateChildViewController
{
UIViewController *childVC = [[UIViewController alloc] init];
}
#end
Now i need to stop interfaceOrientation of childVC.
Is it possible, if so how??
Try using:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
to any view controller to determine auto rotate behaviour.
From the documentation:
By default, this method returns YES for the UIInterfaceOrientationPortrait orientation only. If your view controller supports additional orientations, override this method and return YES for all orientations it supports.
In short, if you subclass UIViewController and only want to support portrait, you don't need to do anything. Otherwise you will need to add this method and decide whether to allow rotation to another orientation.
Read the section titled "Handling View Rotations".