hide home indicator for iPhone X - ios

i have an iOS app with video player, when the video is playing (landscape, full screen) i would like to hide the home indicator on iPhone X. I have try with
if (#available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
and also
-(BOOL)prefersHomeIndicatorAutoHidden{
return YES;
}
but no luck. Does anyone have any idea?

When implementing a container view controller, override
childViewControllerForHomeIndicatorAutoHidden() method if you want
one your child view controllers to determine whether to display the
visual indicator. If you do, the system calls the
prefersHomeIndicatorAutoHidden() method of the returned view
controller. If the method returns nil, the system calls the
prefersHomeIndicatorAutoHidden() method of the current view controller
So if you are using childViewController then need to implement childViewControllerForHomeIndicatorAutoHidden
as -
Swift
extension UINavigationController {
open override func childViewControllerForHomeIndicatorAutoHidden() -> UIViewController? {
return DemoViewController.loadFromNib()
}
}
//DemoViewController is childViewController
class DemoViewController: UIViewController {
static func loadFromNib() -> DemoViewController{
let storyBoardInst = UIStoryboard(name: "Main", bundle: nil)
return storyBoardInst.instantiateViewController(withIdentifier: "DemoViewController") as! DemoViewController
}
override func prefersHomeIndicatorAutoHidden() -> Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
view.backgroundColor = .red
if #available(iOS 11.0, *) {
//Notifies UIKit that your view controller updated its preference regarding the visual indicator
setNeedsUpdateOfHomeIndicatorAutoHidden()
}
}
}
Objective C-
#interface UINavigationController(custom)
#end
#implementation UINavigationController(custom)
-(UIViewController *)childViewControllerForHomeIndicatorAutoHidden{
return [self.storyboard instantiateViewControllerWithIdentifier:#"DemoViewController"];
}
#end
//DemoViewController is childViewController
#interface DemoViewController ()
#end
#implementation DemoViewController
-(BOOL)prefersHomeIndicatorAutoHidden{
return YES;
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:YES];
self.view.backgroundColor = [UIColor redColor];
//Notifies UIKit that your view controller updated its preference
// regarding the visual indicator
if (#available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
}
Output -

Related

Open UIViewController upon clicking on UITabbarItem

I have created a UIViewController with a UITabbar in it.
I did not use UITabbarController because I wanted UITabbar on the top of the screen.
Upon clicking tab1, I want to present controller1 and on clicking tab2 I want to present controller 2. I don't want the tabbar to hide. I want to display the controller beneath the tabbar.
#interface MTLeaderFactoViewController () <UITabBarDelegate>
#property (weak, nonatomic) IBOutlet UITabBar *tabBar;
#end
#implementation MTLeaderFactoViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item {
if (item.tag == 0) {
NSLog(#"item tag 0");
} else {
NSLog(#"item tag 1");
}
}
#end
My questions:
1) didSelectItem method is not triggered even after using UITabbarDelegate
2) What is the most elegant way of displaying the controller when clicked on a button? I don't want to use segue as all the controllers are in different storyboards.
For now, I plan to do
Controller1 *fp = [Controller1 controllerStoryboard:STORYBOARD_COURSE];
[self addChildViewController:fp];
[self.view addSubview:fp.view];
[fp didMoveToParentViewController:self];
EDIT 1:
Controller1 *fp = [Controller1 controllerStoryboard:STORYBOARD_COURSE];
[self addChildViewController:fp];
[self.view addSubview:fp.view];
[fp didMoveToParentViewController:self];
Tried this but it hides the tab bar. I want to utilize the space beneath the tab bar to display the controller
What you need to do is have a basecontroller class which will contain a tabbar(programatically created) then you can achive the desired output heres a sample baseController that i created,
import UIKit
class BaseViewController: UIViewController,UITabBarDelegate{
override func viewDidLoad() {
super.viewDidLoad()
let myTabBar = UITabBar()
myTabBar.frame = CGRect(x: 0, y: 60, width:self.view.frame.size.width, height: 50)
let one = UITabBarItem()
one.title = "one"
one.tag = 1
let two = UITabBarItem()
two.title = "two"
two.tag = 2
myTabBar.setItems([one,two], animated: false)
self.view.addSubview(myTabBar)
myTabBar.delegate = self
}
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
switch item.tag {
case 1:
let controller = storyboard?.instantiateViewController(withIdentifier: "SecondViewController")
addChildViewController(controller!)
view.addSubview((controller?.view)!)
controller?.didMove(toParentViewController: self)
break
case 2:
let controller = storyboard?.instantiateViewController(withIdentifier: "ViewController")
addChildViewController(controller!)
view.addSubview((controller?.view)!)
controller?.didMove(toParentViewController: self)
break
default:
break
}
}
}
View Controller class :
import UIKit
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
}
SecondView Controller :
import UIKit
class SecondViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
I would suggest using UITabBarController instead of UIViewController.
Add UITabViewController in your StoryBoard name it FirstTabBarController.
Add child view controller to FirstTabBarController. In all your child view of add UITabbar. Connect the TabBar delegate to each of your child ViewController.
Hide the default TabBar in your default FirstTabBarController.
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.tabBar.isHidden = true;
[self setSelectedIndex:1];
}
Add action to TabBar in your child ViewController as
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item {
NSUInteger index = [[theTabBar items] indexOfObject:item];
NSLog(#"Tab index = %u", (int)indexO);
[self.navigationController.tabBarController setSelectedIndex:index];
}
I would prefer this method over manually adding adding or removing ViewController as subview, let the UITabBarController manage that for you. Do let me know if you have further queries.

Determine viewWillAppear from Popped UINavigationController or UITabBarController

I am unable to find a way to distinguish between popping from the Nav controller stack and entering the view controller from the UITabBarController.
I want to call a method in ViewWillAppear only when the view is presented from the TabBar, not when someone presses back in the navigation controller.
If I wasn't using a TabBarController, I could easily get this functionally using viewDidLoad.
I've tried,
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
println("View Will Appear")
if isBeingPresented() {
println("BP")
}
if isMovingFromParentViewController() {
println("from")
}
if isMovingToParentViewController() {
println("to")
}
}
But there is no difference when I present from pressing the Tab Button or when press back button.
Only the "View Will Appear" is getting called.
Using iOS 8.4 / Swift
Sounds like a good use of the UITabBarControllerDelegate.
First, add a Bool property on your ViewController comingFromTab:
class MyViewController: UIViewController {
var comingFromTab = false
// ...
}
Set your UITabBarControllerDelegate to whatever class you want and implement the method shouldSelectViewController. You may also want to subclass UITabBarController and put them in there.
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
if let myViewController = viewController as? MyViewController {
myViewController.comingFromTab = true
}
If your tab's initial view controller is a UINavigationController, you will have to unwrap that and access it's first view controller:
if let navController = viewController as? UINavigationController {
if let myViewController = navController.viewControllers[0] as? MyViewController {
// do stuff
}
}
Lastly, add whatever functionality you need in viewWillAppear in your view controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// ...
if comingFromTab {
// Do whatever you need to do here if coming from the tab selection
comingFromTab = false
}
}
There is no way to know for sure. So I guess the easiest way is to add some variable that you will have to change before popping back to that view controller and checking it's state in viewWillAppear.
class YourViewController: UIViewController {
var poppingBack = false
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if !poppingBack {
// your logic
}
else {
poppingBack = false // reset it for next time
}
}
}
// somewhere else in code, suppose yourVC is YourViewController
yourVC.poppingBack = true
self.navigationController.popToViewController(yourVC, animated: true)
You can also try implementing UINavigationControllerDelegate's - navigationController:willShowViewController:animated: method and check if it will be called when presenting your view controller from tab bar.
You can check parentViewController property
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if parentViewController is UITabBarController {
// Presented by UITabBarController
} else if parentViewController is UINavigationController {
// Presented by UINavigationController
} else {
// Presented by ...
}
}

Tap tab bar to scroll to top of UITableViewController

Tapping the tab bar icon for the current navigation controller already returns the user to the root view, but if they are scrolled way down, if they tap it again I want it to scroll to the top (same effect as tapping the status bar). How would I do this?
A good example is Instagram's feed, scroll down then tap the home icon in the tab bar to scroll back to top.
The scrolling back to the top is easy, but connecting it to the tab bar controller is what I'm stuck on.
Implement the UITabBarControllerDelegate method tabBarController:didSelectViewController: to be notified when the user selects a tab. This method is also called when the same tab button is tapped again, even if that tab is already selected.
A good place to implement this delegate would probably be your AppDelegate. Or the object that logically "owns" the tab bar controller.
I would declare and implement a method that can be called on your view controllers to scroll the UICollectionView.
- (void)tabBarController:(UITabBarController *)tabBarController
didSelectViewController:(UIViewController *)viewController
{
static UIViewController *previousController = nil;
if (previousController == viewController) {
// the same tab was tapped a second time
if ([viewController respondsToSelector:#selector(scrollToTop)]) {
[viewController scrollToTop];
}
}
previousController = viewController;
}
SWIFT 3
Here goes..
First implement the UITabBarControllerDelegate in the class and make sure the delegate is set in viewDidLoad
class DesignStoryStreamVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UITabBarControllerDelegate {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
collectionView.delegate = self
collectionView.dataSource = self
}
}
Next, put this delegate function somewhere in your class.
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
print(tabBarIndex)
if tabBarIndex == 0 {
self.collectionView.setContentOffset(CGPoint.zero, animated: true)
}
}
Make sure to select the correct index in the "if" statement. I included the print function so you can double check.
Swift 5: no need for stored properties in the UITabBarController.
In MyTabBarController.swift, implement tabBarController(_:shouldSelect) to detect when the user re-selects the tab bar item:
protocol TabBarReselectHandling {
func handleReselect()
}
class MyTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
func tabBarController(
_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController
) -> Bool {
if tabBarController.selectedViewController === viewController,
let handler = viewController as? TabBarReselectHandling {
// NOTE: viewController in line above might be a UINavigationController,
// in which case you need to access its contents
handler.handleReselect()
}
return true
}
}
In MyTableViewController.swift, handle the re-selection by scrolling the table view to the top:
class MyTableViewController: UITableViewController, TabBarReselectHandling {
func handleReselect() {
tableView?.setContentOffset(.zero, animated: true)
}
}
Now you can easily extend this to other tabs by just implementing TabBarReselectHandling.
You can use shouldSelect rather than didSelect, which would omit the need for an external variable to keep track of the previous view controller.
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
if ([viewController isEqual:self] && [tabBarController.selectedViewController isEqual:viewController]) {
// Do custom stuff here
}
return YES;
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: self.view)
}
}
This is my answer in Swift 3. It uses a helper function for recursive calls and it automatically scrolls to top on call. Tested on a UICollectionViewController embedded into a UINavigationController embedded in a UITabBarController
I was using this View hierarchy.
UITabBarController > UINavigationController > UIViewController
I got a reference to the UITabBarController in the UIViewController
tabBarControllerRef = self.tabBarController as! CustomTabBarClass
tabBarControllerRef!.navigationControllerRef = self.navigationController as! CustomNavigationBarClass
tabBarControllerRef!.viewControllerRef = self
Then I created a Bool that was called at the correct times, and a method that allows scrolling to top smoothly
var canScrollToTop:Bool = true
// Called when the view becomes available
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
canScrollToTop = true
}
// Called when the view becomes unavailable
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
canScrollToTop = false
}
// Scrolls to top nicely
func scrollToTop() {
self.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
Then in my UITabBarController Custom Class I called this
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
// Allows scrolling to top on second tab bar click
if (viewController.isKindOfClass(CustomNavigationBarClass) && tabBarController.selectedIndex == 0) {
if (viewControllerRef!.canScrollToTop) {
viewControllerRef!.scrollToTop()
}
}
}
The Result is identical to Instagram and Twitter's feed :)
Swift 3 approach::
//MARK: Properties
var previousController: UIViewController?
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if self.previousController == viewController || self.previousController == nil {
// the same tab was tapped a second time
let nav = viewController as! UINavigationController
// if in first level of navigation (table view) then and only then scroll to top
if nav.viewControllers.count < 2 {
let tableCont = nav.topViewController as! UITableViewController
tableCont.tableView.setContentOffset(CGPoint(x: 0.0, y: -tableCont.tableView.contentInset.top), animated: true)
}
}
self.previousController = viewController;
return true
}
A few notes here::
"shouldSelect" instead of "didSelect" because the latter is taking place after transition, meaning viewController local var already changed.
2. We need to handle the event before changing controller, in order to have the information of navigation's view controllers regarding scrolling (or not) action.
Explanation:: We want to scroll to top, if current view is actually a List/Table view controller. If navigation has advanced and we tap same tab bar, desired action would be to just pop one step (default functionality) and not scroll to top. If navigation hasn't advanced meaning we are still in table/list controller then and only then we want to scroll to top when tapping again. (Same thing Facebook does when tapping "Feed" from a user's profile. It only goes back to feed without scrolling to top.
In this implementation you no need static variable and previous view controller state
If your UITableViewController in UINavigationController you can implement protocol and function:
protocol ScrollableToTop {
func scrollToTop()
}
extension UIScrollView {
func scrollToTop(_ animated: Bool) {
var topContentOffset: CGPoint
if #available(iOS 11.0, *) {
topContentOffset = CGPoint(x: -safeAreaInsets.left, y: -safeAreaInsets.top)
} else {
topContentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
}
setContentOffset(topContentOffset, animated: animated)
}
}
Then in your UITableViewController:
class MyTableViewController: UITableViewController: ScrollableToTop {
func scrollToTop() {
if isViewLoaded {
tableView.scrollToTop(true)
}
}
}
Then in UITabBarControllerDelegate:
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard tabBarController.selectedViewController === viewController else { return true }
guard let navigationController = viewController as? UINavigationController else {
assertionFailure()
return true
}
guard
navigationController.viewControllers.count <= 1,
let destinationViewController = navigationController.viewControllers.first as? ScrollableToTop
else {
return true
}
destinationViewController.scrollToTop()
return false
}
}
I have a collection view embedded in a navigation controller, in Swift this works.
var previousController: UIViewController?
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if previousController == viewController {
if let navVC = viewController as? UINavigationController, vc = navVC.viewControllers.first as? UICollectionViewController {
vc.collectionView?.setContentOffset(CGPointZero, animated: true)
}
}
previousController = viewController;
}
I've implemented a plug & play UITabBarController that you can freely re-use in your projects. To enable the scroll-to-top functionality, you should just have to use the subclass, nothing else.
Should work out of the box with Storyboards also.
Code:
/// A UITabBarController subclass that allows "scroll-to-top" gestures via tapping
/// tab bar items. You enable the functionality by simply subclassing.
class ScrollToTopTabBarController: UITabBarController, UITabBarControllerDelegate {
/// Determines whether the scrolling capability's enabled.
var scrollEnabled: Bool = true
private var previousIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
/*
Always call "super" if you're overriding this method in your subclass.
*/
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
guard scrollEnabled else {
return
}
guard let index = viewControllers?.indexOf(viewController) else {
return
}
if index == previousIndex {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), { [weak self] () in
guard let scrollView = self?.iterateThroughSubviews(self?.view) else {
return
}
dispatch_async(dispatch_get_main_queue(), {
scrollView.setContentOffset(CGPointZero, animated: true)
})
})
}
previousIndex = index
}
/*
Iterates through the view hierarchy in an attempt to locate a UIScrollView with "scrollsToTop" enabled.
Since the functionality relies on "scrollsToTop", it plugs easily into existing architectures - you can
control the behaviour by modifying "scrollsToTop" on your UIScrollViews.
*/
private func iterateThroughSubviews(parentView: UIView?) -> UIScrollView? {
guard let view = parentView else {
return nil
}
for subview in view.subviews {
if let scrollView = subview as? UIScrollView where scrollView.scrollsToTop == true {
return scrollView
}
if let scrollView = iterateThroughSubviews(subview) {
return scrollView
}
}
return nil
}
}
Edit (09.08.2016):
After attempting to compile with the default Release configuration (archiving) the compiler would not allow the possibility of creating a large number of closures that were captured in a recursive function, thus it would not compile. Changed out the code to return the first found UIScrollView with "scrollsToTop" set to true without using closures.
I tried the solution given by #jsanabria. This worked well on a fixed tableview, but it wouldn't work for my infinite scroll tableview. It only came up the table view about halfway after loading the new scrolling data.
Swift 5.0+
self.tableView.scrollToRow(at: IndexPath.init(row: 0, section: 0), at: UITableView.ScrollPosition(rawValue: 0)!, animated: true)
TESTED SOLUTION IN SWIFT
STEP 1
In your main tabbarcontroller class declare
weak static var previousController: UIViewController?
STEP 2
In viewdidLoad() set
MainTabBarViewController.previousController = viewControllers?[0]
STEP 3
extension MainTabBarViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if MainTabBarViewController.previousController == viewController {
/// here comes your code
}
MainTabBarViewController.previousController = viewController
}
}
I found the scrollRectToVisible method works better than the setContentOffset.
Swift:
After you catch the click on the tab bar from the delegate, something like below:
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if (viewController.isKindOfClass(SomeControllerClass) && tabBarController.selectedIndex == 0)
{
viewController.scrollToTop()
}
}
Now for the scrollToTop function inside the controller:
func scrollToTop()
{
self.tableView.scrollRectToVisible(CGRectMake(0,0,CGRectGetWidth(self.tableView.frame), CGRectGetHeight(self.tableView.frame)), animated: true)
}

How to make UIImagePickerController StatusBar lightContent style?

When I present the ImagePickerController the statusBar text color is still black, how to make like this?
Just three steps:
1: Add UINavigationControllerDelegate,UIImagePickerControllerDelegate to your
#interface yourController ()<>
2: imagePickerController.delegate = self;
3:
-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
}
Swift solution by writing an extension for UIImagePickerController:
extension UIImagePickerController {
convenience init(navigationBarStyle: UIBarStyle) {
self.init()
self.navigationBar.barStyle = navigationBarStyle
}
}
Then you can set the color when initializing it:
let picker = UIImagePickerController(navigationBarStyle: .black) // black bar -> white text
Alternative (inspired by folse's answer): When you initialize the UIImagePickerController normally, make this class the delegate (picker.delegate = self) and implement this function:
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
if navigationController is UIImagePickerController { // check just to be safe
navigationController.navigationBar.barStyle = .black // black bar -> white text
}
}
In Swift and iOS 9, setStatusBarStyle is deprecated. You could subclass the controller.
private final class LightStatusImagePickerController: UIImagePickerController {
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .lightContent
}
}
I had the same problem having to manage the application runned under different iOS versions.
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc] init];
if(IS_IOS8_AND_UP) {
imagePickerController.modalPresentationStyle = UIModalPresentationFullScreen;
} else {
imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
}
imagePickerController.delegate = self;
[self presentViewController:imagePickerController animated:YES completion:nil];
The, in delegate:
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
/* Cancel button color */
_imagePicker.navigationBar.tintColor = <custom_color>
/* Status bar color */
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
}
Using the answers above the following worked for me:
Implement
UINavigationControllerDelegate, UIImagePickerControllerDelegate to your UIViewController
and set
imagePickerController.delegate = self;
Add the following method:
-(void) navigationController: (UINavigationController *) navigationController willShowViewController: (UIViewController *) viewController animated: (BOOL) animated {
navigationController.navigationBar.barStyle = UIBarStyleBlack;
}
I was facing a similar problem and I found the cleanest way to solve it was to override preferredStatusBarStyle in an extension of UIImagePickerController like so. This principal can be applied to third party libraries nicely.
extension UIImagePickerController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
if isLightTheme() {
return .default // black text
}
return .lightContent // white text
}
}
isLightTheme() is simply a function to determine whether the NavigationBar in that controller is a light or dark colour.
This is the quickest solution I could think of. Create the following category:
#implementation UIImagePickerController (LightStatusBar)
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
#end

Presenting a modal controller without knowing the current view controller?

Is there a way to present a view controller modally without knowing what the visible view controller view is? Basically sort of like you would show an alert view at any points in time.
I would like to be able to do something like:
MyViewController *myVC = [[MyViewController alloc] init];
[myVC showModally];
I'd like to be able to call this from anywhere in the app, and have it appear on top. I don't want to care about what the current view controller is.
I plan to use this to show a login prompt. I don't want to use an alert view, and I also don't want to have login presentation code throughout the app.
Any thoughts on this? Or is there maybe a better way to achieve this? Should I just implement my own mechanism and just place a view on top of the window?
Well, you can follow the chain.
Start at [UIApplication sharedApplication].delegate.window.rootViewController.
At each view controller perform the following series of test.
If [viewController isKindOfClass:[UINavigationController class]], then proceed to [(UINavigationController *)viewController topViewController].
If [viewController isKindOfClass:[UITabBarController class]], then proceed to [(UITabBarController *)viewController selectedViewController].
If [viewController presentedViewController], then proceed to [viewController presentedViewController].
My solution in Swift (inspired by the gist of MartinMoizard)
extension UIViewController {
func presentViewControllerFromVisibleViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
if let navigationController = self as? UINavigationController {
navigationController.topViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else if let tabBarController = self as? UITabBarController {
tabBarController.selectedViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else if let presentedViewController = presentedViewController {
presentedViewController.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else {
present(viewControllerToPresent, animated: flag, completion: completion)
}
}
}
This solution gives you the top most view controller so that you can handle any special conditions before presenting from it. For example, maybe you want to present your view controller only if the top most view controller isn't a specific view controller.
extension UIApplication {
/// The top most view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
With this you can present your view controller from anywhere without needing to know what the top most view controller is
UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)
Or present your view controller only if the top most view controller isn't a specific view controller
if let topVC = UIApplication.topMostViewController, !(topVC is FullScreenAlertVC) {
topVC.present(viewController, animated: true, completion: nil)
}
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else if self is UIAlertController {
return nil
} else {
return self
}
}
}
You could have this code implemented in your app delegate:
AppDelegate.m
-(void)presentViewControllerFromVisibleController:(UIViewController *)toPresent
{
UIViewController *vc = self.window.rootViewController;
[vc presentViewController:toPresent animated:YES];
}
AppDelegate.h
-(void)presentViewControllerFromVisibleViewController:(UIViewController *)toPresent;
From Wherever
#import "AppDelegate.h"
...
AppDelegate *delegate = [UIApplication sharedApplication].delegate;
[delegate presentViewControllerFromVisibleViewController:myViewControllerToPresent];
In your delegate, you're getting the rootViewController of the window. This will always be visible- it's the 'parent' controller of everything.
I don't think you necessarily need to know which view controller is visible. You can get to the keyWindow of the application and add your modal view controller's view to the top of the list of views. Then you can make it work like the UIAlertView.
Interface file: MyModalViewController.h
#import <UIKit/UIKit.h>
#interface MyModalViewController : UIViewController
- (void) show;
#end
Implementation file: MyModalViewController.m
#import "MyModalViewController.h"
#implementation MyModalViewController
- (void) show {
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
// Configure the frame of your modal's view.
[window addSubview: self.view];
}
#end

Resources