I have an app targeted iOS8 and initial view controller is UISplitViewController. I use storyboard, so that it kindly instantiate everything for me.
Because of my design I need SplitViewController to show both master and detail views in portrait mode on iPhone. So I am looking for a way to override trait collection for this UISplitViewController.
I found that I can use
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) { ... }
but, unfortunately, there are only methods to override child controllers traits collections:
setOverrideTraitCollection(collection: UITraitCollection!, forChildViewController childViewController: UIViewController!)
and I can't do so for self in my UISplitViewController subclass.
I checked an example app Adaptive Photos from Apple. And in this app author use special TraitOverrideViewController as root and some magic in his viewController setter to make it all works.
It looks horrible for me. Is there are any way around to override traits? Or If there are not, how can I manage to use the same hack with storyboard? In other words, how to inject some viewController as root one only to handle traits for my UISplitViewController with storyboard?
Ok, I wish there was another way around this, but for now I just converted code from the Apple example to Swift and adjusted it to use with Storyboards.
It works, but I still believe it is an awful way to archive this goal.
My TraitOverride.swift:
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMoveToParentViewController(nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMoveToParentViewController(self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
setForcedTraitForSize(size)
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.Phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
case (.Pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
performSegueWithIdentifier("toSplitVC", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "toSplitVC" {
let destinationVC = segue.destinationViewController as UIViewController
viewController = destinationVC
}
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}
To make it work you need to add a new UIViewController on the storyboard and made it the initial. Add show segue from it to your real controller like this:
You need to name the segue "toSplitVC":
and set initial controller to be TraitOverride:
Now it should work for you too. Let me know if you find a better way or any flaws in this one.
I understand that you wanted a SWIFT translation here... And you've probably solved that.
Below is something I've spent a considerable time trying to resolve - getting my SplitView to work on an iPhone 6+ - this is a Cocoa solution.
My Application is TabBar based and the SplitView has Navigation Controllers. In the end my issue was that setOverrideTraitCollection was not being sent to the correct target.
#interface myUITabBarController ()
#property (nonatomic, retain) UITraitCollection *overrideTraitCollection;
#end
#implementation myUITabBarController
- (void)viewDidLoad
{
[super viewDidLoad];
[self performTraitCollectionOverrideForSize:self.view.bounds.size];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
[self performTraitCollectionOverrideForSize:size];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
- (void)performTraitCollectionOverrideForSize:(CGSize)size
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
_overrideTraitCollection = nil;
if (size.width > 320.0)
{
_overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:self];
for (UIViewController * view in self.childViewControllers)
{
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:view];
NSLog(#"myUITabBarController %# AFTER viewTrait=%#", NSStringFromSelector(_cmd), [view traitCollection]);
}
}
#end
UPDATE:
Apple do not recommend doing this:
Use the traitCollection property directly. Do not override it. Do not
provide a custom implementation.
I'm not overriding this property anymore! Now I'm calling overrideTraitCollectionForChildViewController: in the parent viewControler class.
Old answer:
I know it's more than a year since question was asked, but i think my answer will help someone like me who do not achieved success with the accepted answer.
Whell the solution is really simple, you can just override traitCollection: method. Here is an example from my app:
- (UITraitCollection *)traitCollection {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
return super.traitCollection;
} else {
switch (self.modalPresentationStyle) {
case UIModalPresentationFormSheet:
case UIModalPresentationPopover:
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
default:
return super.traitCollection;
}
}
}
the idea is to force Compact size class on iPad if controller is presented as popover or form sheet.
Hope it helps.
The extra top level VC works well for a simple app but it won't propagate down to modally presented VC's as they don't have a parentVC. So you need to insert it again in different places.
A better approach I found was just to subclass UINavigationController and then just use your subclass in the storyboard and elsewhere where you would normally use UINavigationController. It saves the additional VC clutter in storyboards and also saves extra clutter in code.
This example will make all iPhones use regular horizontal size class for landscape.
#implementation MyNavigationController
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
UIDevice *device = [UIDevice currentDevice];
if (device.userInterfaceIdiom == UIUserInterfaceIdiomPhone && CGRectGetWidth(childViewController.view.bounds) > CGRectGetHeight(childViewController.view.bounds)) {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
return nil;
}
#end
Yes, it must use custom container View Controller to override the function viewWillTransitionToSize. You use the storyboard to set the container View Controller as initial.
Also, you can refer this good artical which use the program to implement it. According to it, your judgement portait could have some limitations:
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
other than
if **size.width > size.height**{
self.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClass.Regular), forChildViewController: viewController)
}
else{
self.setOverrideTraitCollection(nil, forChildViewController: viewController)
}
"
Props To #Ilyca
Swift 3
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(size: view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMove(toParentViewController: nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
setForcedTraitForSize(size: size)
super.viewWillTransition(to: size, with: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .regular)
case (.pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
performSegue(withIdentifier: "toSplitVC", sender: self)
}
override var shouldAutomaticallyForwardAppearanceMethods: Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}
Related
I have presented a custom UIWindow in order to show some screens. When I hide the window, the window instance doesn't free up. The underlying view controller also not getting released.
Custom window logic :
class SDKWindow: UIWindow {
// Unique tag to find the SDKWindow in the containers. Iterate all the windows and find the sdk window to show/hide any controller.
static let SDKWindowTag = 101011
init(rootController: UIViewController) {
if #available(iOS 13.0, *) {
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
if let windowScene = windowScene as? UIWindowScene {
super.init(windowScene: windowScene)
} else {
super.init(frame: UIScreen.main.bounds)
}
} else {
super.init(frame: UIScreen.main.bounds)
}
self.rootViewController = rootController
self.windowLevel = .statusBar - 1
self.tag = SDKWindow.SDKWindowTag
self.accessibilityLabel = "SDK.Window"
self.accessibilityViewIsModal = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func show() {
printLog("Show sdkwindow")
self.makeKeyAndVisible()
if self.isHidden == false {
return
}
self.isHidden = false
self.alpha = 1.0
}
func hide() {
if #available(iOS 13.0, *) {
self.windowScene = nil
} else {
self.rootViewController = nil
}
self.alpha = 0.0
self.isHidden = true
self.resignKey()
self.removeFromSuperview()
}
}
When the window presented is hidden
#objc public class MyClass: NSObject {
/// Private variables
private var popupWindow: SDKWindow?
public func showWindow() {
let viewController : CustomController = CustomController.init()
self.popupWindow = SDKWindow.init(rootController: viewController)
self.popupWindow?.show()
}
public func hide() {
self.popupWindow?.hide()
}
}
When hide method is called, CustomViewController Deinit is not called. But when
public func hide() {
self.popupWindow?.hide()
self.popupWindow = nil
}
is performed, the CustomViewController deinit is called and UIWindow instance is also deallocated in memory.
I wanted to implement my own HUD for a UIViewCntroller and a UIView, so I did this:
protocol ViewHudProtocol {
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
Now I can easily adopt ViewHudProtocol on any UIView to call showLoadingView and hideLoadingView. The problem is I want to use the same protocol for UIViewController, so I did this:
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.view.showLoadingView() //Error: UIView has no member showLoadingView
}
func hideLoadingView() {
self.view.hideLoadingView() //Error: UIView has no member hideLoadingView
}
}
I agree to the error that UIView has not adopted the protocol yet. So I did this:
extension UIView: ViewHudProtocol {}
And it works. Is there a better way to do this? I mean it feels wrong to extend every view with ViewHudProtocol, where not all of them will use it. If I could do something like, "only adopt ViewHudProtocol implicitly for a UIView, if its UIViewController demands for it. Else you could adopt ViewHUDProtocol manually on any UIView when required."
I would solve this with the following approach, using associatedtype, defined only for needed views and/or controllers (tested in Xcode 11.2 / swift 5.1):
protocol ViewHudProtocol {
associatedtype Content : ViewHudProtocol
var content: Self.Content { get }
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
var content: some ViewHudProtocol {
return self
}
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.content.showLoadingView() //NO Error
}
func hideLoadingView() {
self.content.hideLoadingView() //NO Error
}
}
//Usage
extension UITableView: ViewHudProtocol { // only for specific view
}
extension UITableViewController: ViewHudProtocol { // only for specific controller
var content: some ViewHudProtocol {
return self.tableView
}
}
The problem
So you want to constraint the conformance of a UIViewController to the protocol ViewHudProtocol only when the UIViewController.view property conforms to ViewHudProtocol.
I am afraid this is not possible.
Understanding the problem
Let's have a better look at your problem
You have 2 types (UIView and UIViewController) and you want to add to both the same functionalities
func showLoadingView()
func hideLoadingView()
What Mick West teaches us
This kind of scenario is somehow similar to what Mick West faced during the development of the Tony Hawks series Mick West and an elegant solution is described in its article Evolve your hierarchy.
Solution
We can apply that approach to your problem and here's the solution
struct HudViewComponent {
let view: UIView
private let hud: UIView
init(view: UIView) {
self.view = view
self.hud = UIView(frame: view.frame)
self.hud.isHidden = true
self.view.addSubview(hud)
}
func showLoadingView() {
self.hud.isHidden = false
}
func hideLoadingView() {
self.hud.isHidden = true
}
}
protocol HasHudViewComponent {
var hidViewComponent: HudViewComponent { get }
}
extension HasHudViewComponent {
func showLoadingView() {
hidViewComponent.showLoadingView()
}
func hideLoadingView() {
hidViewComponent.hideLoadingView()
}
}
That's it, now you can add the hud functionalities to any Type conforming to HasHudViewComponent.
class SomeView: UIView, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self) }()
}
or
class MyViewController: UIViewController, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self.view) }()
}
Considerations
As you can see the idea is to thinking in terms of components.
You build a component (HudViewComponent) with your hud functionalities. The component only asks for the minimum requirements: it needs a UIView.
Next you define the HasHudViewComponent which states that the current type has a HudViewComponent property.
Finally you can add your hud functionalities to any Type which has a view (UIView, UIViewController, ...) simply conforming your type to HasHudViewComponent.
Notes
You asked an interesting question and I know this does not answers 100% what you were looking for, but by a practical point of view it should provides you with a tool to achieve what you need.
I would have taken this approach:
Create a UIView Class,
setup the view
Declare a shared object.
A function to show the view
A function to remove the view. and then call it in view controllers as IndicatorView.shared.show() , IndicatorView.shared.hide()
import Foundation
import UIKit
import Lottie
class IndicatorView : UIView {
static let shared = IndicatorView()
var loadingAnimation : AnimationView = {
let lottieView = AnimationView()
lottieView.translatesAutoresizingMaskIntoConstraints = false
lottieView.layer.masksToBounds = true
return lottieView
}()
var loadingLabel : UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont(name: "SegoeUI", size: 12)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func show() {
setupLoadingView()
self.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.isHidden = false
self.alpha = 1
}, completion: nil)
applyLottieAnimation()
}
public func hide() {
self.alpha = 1
UIView.animate(withDuration: 0.5, animations: {
self.alpha = 0
}, completion: { _ in
self.isHidden = true
self.removeFromSuperview()
}) }
private func setupLoadingView() {
let controller = UIApplication.shared.keyWindow!.rootViewController!
controller.view.addSubview(self)
//setup your views here
self.setNeedsLayout()
self.reloadInputViews()
}
}
For this particular scenario, a Decorator would work better, and result in a better design:
final class HUDDecorator {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
func showLoadingView() {
// add the spinner
}
func hideLoadingView() {
// remove the spinner
}
}
Using the Decorator would then be as easy as declaring a property for it:
class MyViewController: UIViewController {
lazy var hudDecorator = HUDDecorator(view)
}
This will allow any controller to decide if it wants support for showing/hiding a loading view by simply exposing this property.
Protocols are too invasive for simple tasks like enhancing the looks on a UI component, and they have the disadvantage of forcing all views of a certain class to expose the protocol functionality, while the Decorator approach allows you to decide which view instances to receive the functionality.
I have implemented large titles in my app with the following code:
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y <= 0 {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
} else {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .never
} else {
// Fallback on earlier versions
}
}
self.navigationController?.navigationBar.setNeedsLayout()
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.01, animations: {
self.navigationController?.navigationBar.layoutIfNeeded()
self.view.layoutIfNeeded()
})
}
I am able to successfully toggle between views on a tabbar but when I push a view ontop of the tabbar controller and then pop it off using this code:
_ = self.navigationController?.popViewController(animated: true)
I get this crash when I toggle between views on the tabbar again:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'ERROR: UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset:'
This is not a solution, but a potential thing that you need to investigate in your code. I got this same error message (UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset) and I noticed I was doing something incorrectly.
I got this error message in a SwiftUI app using NavigationView.
The mistake I had made was that ParentView had a Navigation View at the root. Using a NavigationLink I was moving to ChildView, which also had a NavigationView as the root. Here's what it looked like in code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ParentView()
}
}
}
struct ParentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: ChildView()) {
Text("Parent view")
}
}
.navigationTitle("Parent")
}
}
}
struct ChildView: View {
var body: some View {
List {
ForEach(0 ..< 5) { _ in
Text("Child view")
}
}
.navigationTitle("Child")
}
}
Initially this is what ChildView looked like:
struct ChildView: View {
var body: some View {
NavigationView {
List {
ForEach(0 ..< 5) { _ in
Text("Second screen")
}
}
.navigationTitle("Second")
}
}
}
Notice how I was trying to push a view which itself was embedded in a NavigationView. Removing it as shown in the first snippet, took care of the error messages. You can try looking into that, maybe you are doing the same mistake just in UIKit instead of SwiftUI.
I found the solution. You have to set the first navigation controller to not use large titles.
The point is that now UIScrollView has only one observer (navigationController) implementing _scrollViewWillEndDraggingWithVelocity.
if (#available(iOS 11.0, *)) {
self.navigationController.navigationBar.prefersLargeTitles = FALSE;
self.navigationController.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
}
The problem happened when the tableview was still scrolling when I went to another view. I fixed the problem by setting a bool in the scrollViewDidScroll that disables any scrolling when the segue is started.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if viewIsVisible {
if scrollView.contentOffset.y <= 0 {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
} else {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .never
} else {
// Fallback on earlier versions
}
}
self.navigationController?.navigationBar.setNeedsLayout()
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.01, animations: {
self.navigationController?.navigationBar.layoutIfNeeded()
self.view.layoutIfNeeded()
})
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
self.viewIsVisible = false
}
I've the same problem and I fixed it by removing this line from AppDelegate:
UINavigationBar.appearance().prefersLargeTitles = true
and handle prefersLargeTitles inside viewDidLoad in certain UIViewController
I think all of above answers don't really solve the issue and are overcomplicated. I recommend enabling/disabling large titles in each of your UIViewController's subclasses, so they don't use large titles at the same time. Good place to do it is in the viewWillAppear and viewWillDisappear methods
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.largeTitleDisplayMode = .always
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.prefersLargeTitles = false
}
I have been trying to display a toast message on iOS. What I did was when any notification comes, just I took the navigation controller view and added a subview for my toast message and displayed.
UIView *top_view = self.navigationController.view;
[top_view showToast:string];
Everything works fine. However my toast view is not adding over the keyboard(if the keyboard is at the front). Any idea what could be the problem... Little helps may save my time... Thanx..
You can display the toast by adding subview to your main window.
UIWindow *toastDisplaywindow = [[[UIApplication sharedApplication] delegate] window];;
for (UIWindow *testWindow in [[UIApplication sharedApplication] windows])
{
if (![[testWindow class] isEqual:[UIWindow class]])
{
self.toastDisplaywindow = testWindow;
break;
}
}
[toastDisplaywindow showToast:string];
If a keyboard is being displayed, it will be displayed as a separate window, above your usual main window. Hence a check made to find out if the keyboard is being displayed. If it is, then add the toast message on that window, else on the main window.
I found another method in this link, using which you can directly get to the UIView of the keyboard (If required).
You have to add your subview to:
UIWindow *window = [UIApplication sharedApplication].windows.lastObject;
which is on top of the keyboard.
Generally keyboard view is not part of your main window. it appears with new window when you get focused in any text field.
Try the following code to access your keyboard view.
[[[UIApplication sharedApplication] windows] objectAtIndex:1]
Remember, this will only work when you have keyboard on your screen.
Another way is to add a custom UIWindow, then setting it's WindowLevel to +1 of the last window.
Something like this
NSArray *windows = [[UIApplication sharedApplication] windows];
UIWindow *lastWindow = (UIWindow *)[windows lastObject];
myWindow.windowLevel = lastWindow.windowLevel + 1;
Take a look to this topic
https://forums.developer.apple.com/thread/16375
Update for Swift3
UIApplication.shared.windows.last
in iOS9 the answer by Adithya is not work,
UIWindow *window = [UIApplication sharedApplication].windows.lastObject;
work well
Try to add the view as a subview of the following class. This code snippet works for iOS 14 and above. Not sure about older versions. Reference: Toaster Github repo
Use it like:
ToastWindow.shared.addSubView(/your_view/)
public final class ToastWindow: UIWindow {
public static let shared = ToastWindow(frame: UIScreen.main.bounds, mainWindow: UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow } )
override public var rootViewController: UIViewController? {
get {
guard !self.isShowing else {
isShowing = false
return nil
}
guard let firstWindow = UIApplication.shared.delegate?.window else { return nil }
return firstWindow is ToastWindow ? nil : firstWindow?.rootViewController
}
set { /* Do nothing */ }
}
override public var isHidden: Bool {
willSet { isShowing = true }
didSet { isShowing = false }
}
/// Will not return `rootViewController` while this value is `true`. Needed for iOS 13.
private var isShowing = false
/// Returns original subviews. `ToastWindow` overrides `addSubview()` to add a subview to the
/// top window instead itself.
private var originalSubviews = NSPointerArray.weakObjects()
private weak var mainWindow: UIWindow?
// MARK: - Init
public init(frame: CGRect, mainWindow: UIWindow?) {
super.init(frame: frame)
self.mainWindow = mainWindow
self.isUserInteractionEnabled = false
self.gestureRecognizers = nil
self.windowLevel = .init(rawValue: .greatestFiniteMagnitude)
let keyboardWillShowName = UIWindow.keyboardWillShowNotification
let keyboardDidHideName = UIWindow.keyboardDidHideNotification
self.backgroundColor = .clear
self.isHidden = false
NotificationCenter.default.addObserver( self, selector: #selector(self.keyboardWillShow),
name: keyboardWillShowName,
object: nil )
NotificationCenter.default.addObserver( self, selector: #selector(self.keyboardDidHide),
name: keyboardDidHideName,
object: nil )
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented: please use ToastWindow.shared")
}
override public func addSubview(_ view: UIView) {
super.addSubview(view)
self.originalSubviews.addPointer(Unmanaged.passUnretained(view).toOpaque())
self.topWindow()?.addSubview(view)
}
public override func becomeKey() {
super.becomeKey()
mainWindow?.makeKey()
}
// MARK: - Keyboard methods
#objc private func keyboardWillShow() {
guard let topWindow = self.topWindow(),
let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
for subview in subviews {
topWindow.addSubview(subview)
}
}
#objc private func keyboardDidHide() {
guard let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
for subview in subviews {
super.addSubview(subview)
}
}
/// Returns top window that isn't self
private func topWindow() -> UIWindow? {
if let window = UIApplication.shared.windows.last(where: {
ToastWindowKeyboardObserver.shared.didKeyboardShow || $0.isOpaque
}), window !== self {
return window
}
return nil
}
}
final fileprivate class ToastWindowKeyboardObserver {
static let shared = ToastWindowKeyboardObserver()
private(set) var didKeyboardShow: Bool = false
private(set) var keyboardHeight = 0.0
private init() {
let keyboardWillShowName = UIWindow.keyboardWillShowNotification
let keyboardDidHideName = UIWindow.keyboardDidHideNotification
NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillShow),
name: keyboardWillShowName,
object: nil )
NotificationCenter.default.addObserver( self, selector: #selector(keyboardDidHide),
name: keyboardDidHideName,
object: nil )
}
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
keyboardHeight = keyboardRectangle.height
}
didKeyboardShow = true
}
#objc private func keyboardDidHide() {
didKeyboardShow = false
}
}
The current UIViewController on the screen need to response to push-notifications from APNs, by setting some badge views. But how could I get the UIViewController in methodapplication:didReceiveRemoteNotification: of AppDelegate.m?
I tried use self.window.rootViewController to get the current displaying UIViewController, it may be a UINavigationViewController or some other kind of view controller. And I find out that the visibleViewController property of UINavigationViewController can be used to get the UIViewController on the screen. But what could I do if it is not a UINavigationViewController?
Any help is appreciated! The related code is as following.
AppDelegate.m
...
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
//I would like to find out which view controller is on the screen here.
UIViewController *vc = [(UINavigationViewController *)self.window.rootViewController visibleViewController];
[vc performSelector:#selector(handleThePushNotification:) withObject:userInfo];
}
...
ViewControllerA.m
- (void)handleThePushNotification:(NSDictionary *)userInfo{
//set some badge view here
}
I always love solutions that involve categories as they are bolt on and can be easily reused.
So I created a category on UIWindow. You can now call visibleViewController on UIWindow and this will get you the visible view controller by searching down the controller hierarchy. This works if you are using navigation and/or tab bar controller. If you have another type of controller to suggest please let me know and I can add it.
UIWindow+PazLabs.h (header file)
#import <UIKit/UIKit.h>
#interface UIWindow (PazLabs)
- (UIViewController *) visibleViewController;
#end
UIWindow+PazLabs.m (implementation file)
#import "UIWindow+PazLabs.h"
#implementation UIWindow (PazLabs)
- (UIViewController *)visibleViewController {
UIViewController *rootViewController = self.rootViewController;
return [UIWindow getVisibleViewControllerFrom:rootViewController];
}
+ (UIViewController *) getVisibleViewControllerFrom:(UIViewController *) vc {
if ([vc isKindOfClass:[UINavigationController class]]) {
return [UIWindow getVisibleViewControllerFrom:[((UINavigationController *) vc) visibleViewController]];
} else if ([vc isKindOfClass:[UITabBarController class]]) {
return [UIWindow getVisibleViewControllerFrom:[((UITabBarController *) vc) selectedViewController]];
} else {
if (vc.presentedViewController) {
return [UIWindow getVisibleViewControllerFrom:vc.presentedViewController];
} else {
return vc;
}
}
}
#end
Swift Version
public extension UIWindow {
public var visibleViewController: UIViewController? {
return UIWindow.getVisibleViewControllerFrom(self.rootViewController)
}
public static func getVisibleViewControllerFrom(_ vc: UIViewController?) -> UIViewController? {
if let nc = vc as? UINavigationController {
return UIWindow.getVisibleViewControllerFrom(nc.visibleViewController)
} else if let tc = vc as? UITabBarController {
return UIWindow.getVisibleViewControllerFrom(tc.selectedViewController)
} else {
if let pvc = vc?.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(pvc)
} else {
return vc
}
}
}
}
You can use the rootViewController also when your controller is not a UINavigationController:
UIViewController *vc = self.window.rootViewController;
Once you know the root view controller, then it depends on how you have built your UI, but you can possibly find out a way to navigate through the controllers hierarchy.
If you give some more details about the way you defined your app, then I might give some more hint.
EDIT:
If you want the topmost view (not view controller), you could check
[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];
although this view might be invisible or even covered by some of its subviews...
again, it depends on your UI, but this might help...
Simple extension for UIApplication in Swift (cares even about moreNavigationController within UITabBarController on iPhone):
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
let moreNavigationController = tab.moreNavigationController
if let top = moreNavigationController.topViewController where top.view.window != nil {
return topViewController(top)
} else if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
Simple usage:
if let rootViewController = UIApplication.topViewController() {
//do sth with root view controller
}
Works perfect:-)
UPDATE for clean code:
extension UIViewController {
var top: UIViewController? {
if let controller = self as? UINavigationController {
return controller.topViewController?.top
}
if let controller = self as? UISplitViewController {
return controller.viewControllers.last?.top
}
if let controller = self as? UITabBarController {
return controller.selectedViewController?.top
}
if let controller = presentedViewController {
return controller.top
}
return self
}
}
You could also post a notification via NSNotificationCenter. This let's you deal with a number of situations where traversing the view controller hierarchy might be tricky - for example when modals are being presented, etc.
E.g.,
// MyAppDelegate.h
NSString * const UIApplicationDidReceiveRemoteNotification;
// MyAppDelegate.m
NSString * const UIApplicationDidReceiveRemoteNotification = #"UIApplicationDidReceiveRemoteNotification";
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidReceiveRemoteNotification
object:self
userInfo:userInfo];
}
In each of your View Controllers:
-(void)viewDidLoad {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(didReceiveRemoteNotification:)
name:UIApplicationDidReceiveRemoteNotification
object:nil];
}
-(void)viewDidUnload {
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:UIApplicationDidReceiveRemoteNotification
object:nil];
}
-(void)didReceiveRemoteNotification:(NSDictionary *)userInfo {
// see http://stackoverflow.com/a/2777460/305149
if (self.isViewLoaded && self.view.window) {
// handle the notification
}
}
You could also use this approach to instrument controls which need to update when a notification is received and are used by several view controllers. In that case, handle the add/remove observer calls in the init and dealloc methods, respectively.
Code
Here's an approach using the great switch-case syntax in Swift 3/4/5:
import UIKit
extension UIWindow {
/// Returns the currently visible view controller if any reachable within the window.
public var visibleViewController: UIViewController? {
return UIWindow.visibleViewController(from: rootViewController)
}
/// Recursively follows navigation controllers, tab bar controllers and modal presented view controllers starting
/// from the given view controller to find the currently visible view controller.
///
/// - Parameters:
/// - viewController: The view controller to start the recursive search from.
/// - Returns: The view controller that is most probably visible on screen right now.
public static func visibleViewController(from viewController: UIViewController?) -> UIViewController? {
switch viewController {
case let navigationController as UINavigationController:
return UIWindow.visibleViewController(from: navigationController.visibleViewController ?? navigationController.topViewController)
case let tabBarController as UITabBarController:
return UIWindow.visibleViewController(from: tabBarController.selectedViewController)
case let presentingViewController where viewController?.presentedViewController != nil:
return UIWindow.visibleViewController(from: presentingViewController?.presentedViewController)
default:
return viewController
}
}
}
The basic idea is the same as in zirinisp's answer, it's just using a more Swift 3+ like syntax.
Usage
You probably want to create a file named UIWindowExt.swift and copy the above extension code into it.
On the call side it can be either used without any specific view controller:
if let visibleViewCtrl = UIApplication.shared.keyWindow?.visibleViewController {
// do whatever you want with your `visibleViewCtrl`
}
Or if you know your visible view controller is reachable from a specific view controller:
if let visibleViewCtrl = UIWindow.visibleViewController(from: specificViewCtrl) {
// do whatever you want with your `visibleViewCtrl`
}
I hope it helps!
I have found that iOS 8 has screwed everything up. In iOS 7 there is a new UITransitionView on the view hierarchy whenever you have a modally presented UINavigationController. Anyway, here's my code that finds gets the topmost VC. Calling getTopMostViewController should return a VC that you should be able to send a message like presentViewController:animated:completion. It's purpose is to get you a VC that you can use to present a modal VC, so it will most likely stop and return at container classes like UINavigationController and NOT the VC contained within them. Should not be hard to adapt the code to do that too. I've tested this code in various situations in iOS 6, 7 and 8. Please let me know if you find bugs.
+ (UIViewController*) getTopMostViewController
{
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
if (window.windowLevel != UIWindowLevelNormal) {
NSArray *windows = [[UIApplication sharedApplication] windows];
for(window in windows) {
if (window.windowLevel == UIWindowLevelNormal) {
break;
}
}
}
for (UIView *subView in [window subviews])
{
UIResponder *responder = [subView nextResponder];
//added this block of code for iOS 8 which puts a UITransitionView in between the UIWindow and the UILayoutContainerView
if ([responder isEqual:window])
{
//this is a UITransitionView
if ([[subView subviews] count])
{
UIView *subSubView = [subView subviews][0]; //this should be the UILayoutContainerView
responder = [subSubView nextResponder];
}
}
if([responder isKindOfClass:[UIViewController class]]) {
return [self topViewController: (UIViewController *) responder];
}
}
return nil;
}
+ (UIViewController *) topViewController: (UIViewController *) controller
{
BOOL isPresenting = NO;
do {
// this path is called only on iOS 6+, so -presentedViewController is fine here.
UIViewController *presented = [controller presentedViewController];
isPresenting = presented != nil;
if(presented != nil) {
controller = presented;
}
} while (isPresenting);
return controller;
}
Way less code than all other solutions:
Objective-C version:
- (UIViewController *)getTopViewController {
UIViewController *topViewController = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
while (topViewController.presentedViewController) topViewController = topViewController.presentedViewController;
return topViewController;
}
Swift 2.0 version: (credit goes to Steve.B)
func getTopViewController() -> UIViewController {
var topViewController = UIApplication.sharedApplication().delegate!.window!!.rootViewController!
while (topViewController.presentedViewController != nil) {
topViewController = topViewController.presentedViewController!
}
return topViewController
}
Works anywhere in your app, even with modals.
zirinisp's Answer in Swift:
extension UIWindow {
func visibleViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.getVisibleViewControllerFrom(rootViewController)
}
return nil
}
class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
if vc.isKindOfClass(UINavigationController.self) {
let navigationController = vc as UINavigationController
return UIWindow.getVisibleViewControllerFrom( navigationController.visibleViewController)
} else if vc.isKindOfClass(UITabBarController.self) {
let tabBarController = vc as UITabBarController
return UIWindow.getVisibleViewControllerFrom(tabBarController.selectedViewController!)
} else {
if let presentedViewController = vc.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(presentedViewController.presentedViewController!)
} else {
return vc;
}
}
}
}
Usage:
if let topController = window.visibleViewController() {
println(topController)
}
Specify title to each ViewController and then get the title of current ViewController by the code given below.
-(void)viewDidUnload {
NSString *currentController = self.navigationController.visibleViewController.title;
Then check it by your title like this
if([currentController isEqualToString:#"myViewControllerTitle"]){
//write your code according to View controller.
}
}
Mine is better! :)
extension UIApplication {
var visibleViewController : UIViewController? {
return keyWindow?.rootViewController?.topViewController
}
}
extension UIViewController {
fileprivate var topViewController: UIViewController {
switch self {
case is UINavigationController:
return (self as! UINavigationController).visibleViewController?.topViewController ?? self
case is UITabBarController:
return (self as! UITabBarController).selectedViewController?.topViewController ?? self
default:
return presentedViewController?.topViewController ?? self
}
}
}
Why not just handle the push notification code in the app delegate? Is it directly related to a view?
You can check if a UIViewController's view is currently visible by checking if it's view's window property has a value. See more here.
Just addition to #zirinisp answer.
Create a file, name it UIWindowExtension.swift and paste the following snippet:
import UIKit
public extension UIWindow {
public var visibleViewController: UIViewController? {
return UIWindow.getVisibleViewControllerFrom(self.rootViewController)
}
public static func getVisibleViewControllerFrom(vc: UIViewController?) -> UIViewController? {
if let nc = vc as? UINavigationController {
return UIWindow.getVisibleViewControllerFrom(nc.visibleViewController)
} else if let tc = vc as? UITabBarController {
return UIWindow.getVisibleViewControllerFrom(tc.selectedViewController)
} else {
if let pvc = vc?.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(pvc)
} else {
return vc
}
}
}
}
func getTopViewController() -> UIViewController? {
let appDelegate = UIApplication.sharedApplication().delegate
if let window = appDelegate!.window {
return window?.visibleViewController
}
return nil
}
Use it anywhere as:
if let topVC = getTopViewController() {
}
Thanks to #zirinisp.
Regarding NSNotificationCenter Post above (sorry can't find out where to post a comment under it...)
In case some were getting the -[NSConcreteNotification allKeys] error of sorts. Change this:
-(void)didReceiveRemoteNotification:(NSDictionary *)userInfo
to this:
-(void)didReceiveRemoteNotification:(NSNotification*)notif {
NSDictionary *dict = notif.userInfo;
}
This worked for me. I have many targets that have different controllers so previous answers didn't seemed to work.
first you want this inside your AppDelegate class:
var window: UIWindow?
then, in your function
let navigationController = window?.rootViewController as? UINavigationController
if let activeController = navigationController!.visibleViewController {
if activeController.isKindOfClass( MyViewController ) {
println("I have found my controller!")
}
}
This is the best possible way that I have tried out. If it should help anyone...
+ (UIViewController*) topMostController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
I created a category for UIApplication with visibleViewControllers property. The main idea is pretty simple. I swizzled viewDidAppear and viewDidDisappear methods in UIViewController. In viewDidAppear method viewController is added to stack. In viewDidDisappear method viewController is removed from stack. NSPointerArray is used instead of NSArray to store weak UIViewController’s references . This approach works for any viewControllers hierarchy.
UIApplication+VisibleViewControllers.h
#import <UIKit/UIKit.h>
#interface UIApplication (VisibleViewControllers)
#property (nonatomic, readonly) NSArray<__kindof UIViewController *> *visibleViewControllers;
#end
UIApplication+VisibleViewControllers.m
#import "UIApplication+VisibleViewControllers.h"
#import <objc/runtime.h>
#interface UIApplication ()
#property (nonatomic, readonly) NSPointerArray *visibleViewControllersPointers;
#end
#implementation UIApplication (VisibleViewControllers)
- (NSArray<__kindof UIViewController *> *)visibleViewControllers {
return self.visibleViewControllersPointers.allObjects;
}
- (NSPointerArray *)visibleViewControllersPointers {
NSPointerArray *pointers = objc_getAssociatedObject(self, #selector(visibleViewControllersPointers));
if (!pointers) {
pointers = [NSPointerArray weakObjectsPointerArray];
objc_setAssociatedObject(self, #selector(visibleViewControllersPointers), pointers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return pointers;
}
#end
#implementation UIViewController (UIApplication_VisibleViewControllers)
+ (void)swizzleMethodWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector {
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
BOOL didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleMethodWithOriginalSelector:#selector(viewDidAppear:)
swizzledSelector:#selector(uiapplication_visibleviewcontrollers_viewDidAppear:)];
[self swizzleMethodWithOriginalSelector:#selector(viewDidDisappear:)
swizzledSelector:#selector(uiapplication_visibleviewcontrollers_viewDidDisappear:)];
});
}
- (void)uiapplication_visibleviewcontrollers_viewDidAppear:(BOOL)animated {
[[UIApplication sharedApplication].visibleViewControllersPointers addPointer:(__bridge void * _Nullable)self];
[self uiapplication_visibleviewcontrollers_viewDidAppear:animated];
}
- (void)uiapplication_visibleviewcontrollers_viewDidDisappear:(BOOL)animated {
NSPointerArray *pointers = [UIApplication sharedApplication].visibleViewControllersPointers;
for (int i = 0; i < pointers.count; i++) {
UIViewController *viewController = [pointers pointerAtIndex:i];
if ([viewController isEqual:self]) {
[pointers removePointerAtIndex:i];
break;
}
}
[self uiapplication_visibleviewcontrollers_viewDidDisappear:animated];
}
#end
https://gist.github.com/medvedzzz/e6287b99011f2437ac0beb5a72a897f0
Swift 3 version
UIApplication+VisibleViewControllers.swift
import UIKit
extension UIApplication {
private struct AssociatedObjectsKeys {
static var visibleViewControllersPointers = "UIApplication_visibleViewControllersPointers"
}
fileprivate var visibleViewControllersPointers: NSPointerArray {
var pointers = objc_getAssociatedObject(self, &AssociatedObjectsKeys.visibleViewControllersPointers) as! NSPointerArray?
if (pointers == nil) {
pointers = NSPointerArray.weakObjects()
objc_setAssociatedObject(self, &AssociatedObjectsKeys.visibleViewControllersPointers, pointers, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return pointers!
}
var visibleViewControllers: [UIViewController] {
return visibleViewControllersPointers.allObjects as! [UIViewController]
}
}
extension UIViewController {
private static func swizzleFunc(withOriginalSelector originalSelector: Selector, swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
override open class func initialize() {
if self != UIViewController.self {
return
}
let swizzlingClosure: () = {
UIViewController.swizzleFunc(withOriginalSelector: #selector(UIViewController.viewDidAppear(_:)),
swizzledSelector: #selector(uiapplication_visibleviewcontrollers_viewDidAppear(_:)))
UIViewController.swizzleFunc(withOriginalSelector: #selector(UIViewController.viewDidDisappear(_:)),
swizzledSelector: #selector(uiapplication_visibleviewcontrollers_viewDidDisappear(_:)))
}()
swizzlingClosure
}
#objc private func uiapplication_visibleviewcontrollers_viewDidAppear(_ animated: Bool) {
UIApplication.shared.visibleViewControllersPointers.addPointer(Unmanaged.passUnretained(self).toOpaque())
uiapplication_visibleviewcontrollers_viewDidAppear(animated)
}
#objc private func uiapplication_visibleviewcontrollers_viewDidDisappear(_ animated: Bool) {
let pointers = UIApplication.shared.visibleViewControllersPointers
for i in 0..<pointers.count {
if let pointer = pointers.pointer(at: i) {
let viewController = Unmanaged<AnyObject>.fromOpaque(pointer).takeUnretainedValue() as? UIViewController
if viewController.isEqual(self) {
pointers.removePointer(at: i)
break
}
}
}
uiapplication_visibleviewcontrollers_viewDidDisappear(animated)
}
}
https://gist.github.com/medvedzzz/ee6f4071639d987793977dba04e11399
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 easily get the top post view controller like so
let viewController = UIApplication.topMostViewController
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController.
Swift 2.0 version of jungledev's answer
func getTopViewController() -> UIViewController {
var topViewController = UIApplication.sharedApplication().delegate!.window!!.rootViewController!
while (topViewController.presentedViewController != nil) {
topViewController = topViewController.presentedViewController!
}
return topViewController
}
Always check your build configuration if you you are running your app with debug or release.
IMPORTANT NOTE: You can't be able to test it without running your app in debug mode
This was my solution
In Swift, just add this single line of code to your
BaseViewController to log your displayed view
controllers
override public func viewDidLoad() {
super.viewDidLoad()
print("Current ViewController 🙋🏻♂️ #\(UIApplication.getTopViewController()?.classForCoder ?? self.classForCoder)")
}