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)")
}
Related
I have these protocols:
protocol Coordinator {
var rootViewController: UIViewController { get set }
func start()
}
protocol UIViewControllerFactory {
func mainViewController() -> UIViewController
}
And I created a MainCoordinator that conforms to this protocol and I pass a factory that allows me to decouple the coordinator from creating and capturing a concrete type so it can be polymorphic and can be used with more implementations of UIViewController either as rootViewControllers and mainMenuViewController as shown below:
class MainCoordinator: Coordinator {
var rootViewController: UIViewController
let factory: UIViewControllerFactory
init(rootViewController: UIViewController, factory: UIViewControllerFactory) {
self.rootViewController = rootViewController
}
start() {
guard let mainVC = factory.mainViewController() as? MainViewController, let rootViewController = rootViewController as? UINavigationViewController else { return }
mainVC.delegate = self
rootViewController.push(mainVC, animated: true)
}
As you can see, although I've created the coordinator to accept any subclass of UIViewController it has been coupled in the start function to the concrete implementation of UIViewController: MainViewController.
So my question is how to decouple it from MainViewController and have it more polymorphic?
You can pass coordinator as a parameter type in factory function and set delegate directly in factory function while creating controller instance. That way you wouldn’t have to expose controller type explicitly out of factory classes.
I came up with below approach.
protocol Coordinator {
var rootViewController: UIViewController { get set }
func start()
}
protocol UIViewControllerFactory {
func getViewController(delegateType:CoordinatoreTypes,delegateObject:Coordinator) -> UIViewController?
}
class MainCoordinator: Coordinator {
var rootViewController: UIViewController
let factory: UIViewControllerFactory
init(rootViewController: UIViewController, factory: UIViewControllerFactory) {
self.rootViewController = rootViewController
self.factory = factory
}
func start() {
guard let controller = factory.getViewController(delegateType: .MainCoordinator, delegateObject: self),let rootViewController = rootViewController as? UINavigationViewController else {
return
}
rootViewController.push(mainVC, animated: true)
}
}
extension MainCoordinator:DelegateCaller{
func printHello() {
print("helloo")
}
}
enum CoordinatoreTypes{
case MainCoordinator
case none
}
class Factory:UIViewControllerFactory{
func getViewController(delegateType:CoordinatoreTypes,delegateObject:Coordinator) -> UIViewController?{
switch delegateType{
case .MainCoordinator:
let controller = MainViewController()
controller.delegate = delegateObject as? MainCoordinator
return controller
case .none:
break
}
return nil
}
}
class MainViewController:UIViewController{
weak var delegate:DelegateCaller?
}
protocol DelegateCaller:AnyObject{
func printHello()
}
I am using MVVM with coordinator pattern for navigation. Navigation is made with pushViewController. Tab Bar Controller is created in TabBarCoordinator class.
class TabBarCoordinator: NSObject, BaseCoordinator {
var rootViewController: UIViewController {
return tabBarController
}
var tabBarController: UITabBarController
var window: UIWindow
private let appDependency: AppDependency
var navigationController: NavigationController
private let navigationObserver: NavigationObserver
var childCoordinators: [BaseCoordinator] = []
var onDidFinish: (() -> Void)?
var topController: UIViewController {
if let lastChild = topCoordinator {
return lastChild.topController
}
var controller: UIViewController = navigationController
while let presentedController = controller.presentedViewController {
controller = presentedController
}
return controller
}
func start() {
let homeCoordinator = HomeCoordinator(navigationObserver: navigationObserver,
navigationController: navigationController,
appDependency: appDependency)
add(child: homeCoordinator)
homeCoordinator.start()
let ordersListCoordinator = OrdersListCoordinator(navigationController: navigationController,
navigationObserver: navigationObserver,
appDependency: appDependency,
filterStatus: nil)
add(child: ordersListCoordinator)
ordersListCoordinator.start()
tabBarController.navigationController?.navigationBar.isTranslucent = true
let homeVC = homeCoordinator.topController
homeVC.tabBarItem = UITabBarItem(title: "home", image: nil, selectedImage: nil)
let ordersListVC = ordersListCoordinator.topController
ordersListVC.tabBarItem = UITabBarItem(title: "orders", image: nil, selectedImage: nil)
let controllers = [homeVC, ordersListVC]
tabBarController.viewControllers = controllers.map { UINavigationController(rootViewController: $0) }
window.rootViewController = tabBarController
window.makeKeyAndVisible()
}
required init(window: UIWindow,
navigationObserver: NavigationObserver,
navigationController: NavigationController,
appDependency: AppDependency
) {
tabBarController = UITabBarController()
self.window = window
self.navigationController = navigationController
self.navigationObserver = navigationObserver
self.appDependency = appDependency
}
}
Here the code of func start() for HomeCoordinator class:
func start() {
guard let user = appDependency.userDataStore.authorizedUserInfo?.user else { return }
let viewModel = HomeViewModel(user: user)
viewModel.delegate = self
let homeViewController = HomeViewController(viewModel: viewModel)
viewController = homeViewController
navigationController.viewControllers = [homeViewController]
}
Tab bar Screenshot
Problems with tab bar:
isTranslucent doesn't hide navBar from TabbarController
the screen is black
views of tabBarItems are not shown, but you can still tap them and screens would change
the second vc is just black screen
Any ideas what's wrong with my TabBarCoordinator class? Thank you in advance
If I have this extension:
extension UIResponder {
var parentViewController: UIViewController? {
return (self.next as? UIViewController) ?? self.next?.parentViewController
}
}
And this one:
extension UIView {
var getParentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
If I have a class like this:
class MyClass: UIView {
init() {
print("\( parentViewController == nil )")
print("\( getParentViewController == nil )")
}
}
Both results are null.
How I can get the implemented ViewController from my UIView without pass it as parameter (directly from the view)?
You can’t get the view’s next responder in its init because it hasn’t been set yet. An appropriate time would be in didMoveToWindow.
Also, in your own answer, you said “…is the easier way to get the uiviewcontroller”, as if an app has only one instance of UIViewController, but every non-trivial app has multiple view controllers.
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
}
}
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
}
}