I've got a timer showing an alert when finished.
This alert view should be presented in the view controller which the user is currently in.
My feeling is this could be accomplished much more effective than the following:
The way I'm doing this now is give an observer for a notification to each of my 5 view controllers as well as a method to create and present that alert.
Is there a way to only set up the alert once and then present it in the view controller that is currently active?
Here's my code:
// I've got the following in each of my view controllers.
// In viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SonglistViewController.presentSleepTimerFinishedAlert(_:)), name: "presentSleepTimerFinishedAlert", object: nil)
}
func presentTimerFinishedAlert(notification: NSNotification) {
let alertController = UIAlertController(title: "Timer finished", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
presentViewController(alertController, animated: true, completion: nil)
}
Thanks a lot for any ideas!
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 present your alert like so
UIApplication.topMostViewController?.present(alert, animated: true, completion: nil)
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else if self is UIAlertController {
return nil
} else {
return self
}
}
}
You can find the Top ViewController on the navigation stack and directly present the AlertController from there. You can use the extension method posted here to find the Top ViewController from anywhere in your application:
https://stackoverflow.com/a/30858591/2754727
It really depends on your navigation schema.
First of all you will need current VC. If you've got root view controller as navigation controller and don't show any modals you can get current VC from rootVC. If you've got mixed navigation. i.e. tabbar and then navigation controllers inside, with possible some modals form them you can write an extension on AppDelegate which will search and return current VC.
Now you should pin somewhere this timer class - it may be a singleton or just be pinned somewhere. Than in this timer class, when the timer ends you can look for current VC (using AppDelegate's extension method or referring to your root navigation controller) an present an alert on it.
Related
How would I dismiss a modal View Controller and also its parent that was pushed?
self.presentingViewController?.dismiss(animated: true, completion: {
self.parent?.navigationController?.popViewController(animated: true)
})
This only dismisses the top modal.
You can go another way.
First, you have UINavigationController in your Home view. So you can write an extension that will allow you to go to the controller, which is in the navigation stack.
I tried making an implementation like this:
extension UINavigationController {
func routingPath(for controller: UIViewController) -> [UIViewController] {
guard viewControllers.contains(controller) else {
return []
}
var result: [UIViewController] = []
for previousController in viewControllers {
result.append(previousController)
if controller === previousController {
break
}
}
return result
}
func performNavigation(toPrevious controller: UIViewController,
shouldDismissModals: Bool = true) {
let previousViewControllers = routingPath(for: controller)
guard !previousViewControllers.isEmpty else { return }
viewControllers = previousViewControllers
if shouldDismissModals, let _ = controller.presentedViewController {
controller.dismiss(animated: true, completion: nil)
}
}
}
Then you can make a special method for UIViewController:
extension UIViewController {
func returnBackIfPossible(to controller: UIViewController? = nil,
shouldDismissModals: Bool = true) {
navigationController?.performNavigation(toPrevious: controller ?? self,
shouldDismissModals: shouldDismissModals)
}
}
Then you need to pass a reference for a Home controller to all of the next controllers (or store it somewhere). Next, when needed, you can call to homeViewController?.returnBackIfPossible() method, which will close all modals and reset navigation stack.
What is non-modal parent exectly?
Is it a view controller pushed by the navigation controller?
If then, you must pop that view controller from navigation controller.
I have a custom action sheet viewController. Which is presented modally on the current top view controller. Like this:
//MARK: Static Func
static func initViewController() -> CustomActionSheetViewController {
let customActionSheetViewController = CustomActionSheetViewController(nibName: "CustomActionSheetViewController", bundle: nil)
return customActionSheetViewController
}
func presentViewController<T: UIViewController>(viewController: T) {
DispatchQueue.main.async {
if let topViewController = UIApplication.getTopViewController() {
viewController.modalTransitionStyle = .crossDissolve
viewController.modalPresentationStyle = .overCurrentContext
topViewController.present(viewController, animated: true, completion: nil)
}
}
}
// MARK: UIApplication extensions
extension UIApplication {
class func getTopViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return getTopViewController(base: nav.visibleViewController)
} else if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return getTopViewController(base: selected)
} else if let presented = base?.presentedViewController {
return getTopViewController(base: presented)
}
return base
}
}
And I am dismissing it like this:
#objc func dismissViewController() {
DispatchQueue.main.async {
if let topViewController = UIApplication.getTopViewController() {
topViewController.dismiss(animated: true, completion: nil)
}
NotificationCenter.default.removeObserver(self)
}
}
It's working perfectly fine. I have added the notification observer in my customTabbarController, to dismiss the action sheet if user tap on some another tabbar button like this:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
// print("Selected view controller", viewController)
// print("index", tabBarController.selectedIndex )
let tabbarNotiKey = Notification.Name(rawValue: "TabbarNotiKey")
NotificationCenter.default.post(name: tabbarNotiKey, object: nil, userInfo: nil)
}
The action sheet is right now presenting on Home tab > Profile (by push) > Action sheet (by modal). So if I tap on Home tab again it will dismiss the action sheet viewController and come back to Home perfectly. But if I tap on some other tabbar button rather than home and come back home, it shows a black screen. What I am missing here? Any suggestions would be highly appreciable.
I guess your code calls dismissViewController() twice.
When you press other tab, it removes the action sheet
And then you click home tab and it calls dismissViewController again, and now it removes the homescreenVC
I am implementing Admob Interstitial Admob Ad in my iOS app. I need to show ad between transition from a Second View Controller to a (Main) View Controller. I display Second View Controller as "Present Modally", "Over Current Context". It looks like popup window.
When I return from Second (popup) View Controller (close/hide it), I have an error: Attempt to present GADNFullScreenAdViewController on ViewController which is already presenting SecondViewController.
How to handle this situation in a right way?
#IBAction func afterShowingSecond(_ segue:UIStoryboardSegue) {
if let secondViewController = segue.source as? SecondViewController {
if (secondViewController.someObject != nil) {
if (interstitial.isReady && interstitialAdIntervalExpired && !doNotShowAds) {
interstitial.present(fromRootViewController: self)
return
}
// some code
}
}
}
you can use the function dismiss(animated:completion:)
if let presentingVC = self.presentingViewController {
self.dismiss(animated: true) {
let vc = YourViewController()
presentingVC.present(vc, animated: true, completion: nil)
}
}
I'm trying to let the user create a new contact. While I've got the screen to prompt the user to put in all his details there is no navigation bar at the top(Like there is in the default Apple Contacts app). There is no way to exit the scene. I'm using the ContactUI framework in swift 2.0 and Xcode 7.3. Here's the code:
// create a new contact
let createNewActionHandler = {(action: UIAlertAction) -> Void in
let newContact = CNMutableContact()
let contactPicker = CNContactViewController(forNewContact: newContact)
contactPicker.delegate = self
contactPicker.navigationController?.setToolbarHidden(false, animated: false)
self.presentViewController(contactPicker, animated: true, completion: nil)
}
Here's what I'm trying to get:
Apple Default
Here's what I have:
What I have
I'm launching the new contact view controller from an action sheet in a tab view controller. I tried embedding the tab in a Navigation view controller but to no effect. I even tried setting the setToolbarHidden property of the navController but it didn't help.
Thanks for any help. I saw the issue raised in other forums but they didn't help.
You have to embed contactViewController to UINavigationController
and Implement Delegate Methods.
let createNewActionHandler = {(action: UIAlertAction) -> Void in
let newContact = CNMutableContact()
let contactPicker = CNContactViewController(forNewContact: newContact)
contactPicker.delegate = self
let navigation = UINavigationController(rootViewController: contactPicker)
self.presentViewController(navigation, animated: true, completion: nil)
}
//MARK: - Delegate
func contactViewController(viewController: CNContactViewController, didCompleteWithContact contact: CNContact?) {
viewController.dismissViewControllerAnimated(true, completion: nil)
}
func contactViewController(viewController: CNContactViewController, shouldPerformDefaultActionForContactProperty property: CNContactProperty) -> Bool {
return true
}
The view controllers must be embedded in UINavigationController and you should push or show view controller:
navigationController?.pushViewController(contactPicker, animated: true)
instead of presenting view controller
Since you are presenting contactPicker viewcontroller on top of current active controller, you will not have access to navigationbar as the view is presented fully,if you want to have button's as in Apple contact app you need to embed your presenting viewcontroller inside UINavigationController, and add left and right bar button items.
Refer the following apple sample which demonstrates the same.
https://developer.apple.com/library/ios/samplecode/iPhoneCoreDataRecipes/Listings/Classes_RecipeAddViewController_m.html
I tried all methods but none worked, So I made a custom method
extension UIBarButtonItem {
private struct AssociatedObject {
static var key = "action_closure_key"
}
var actionClosure: (()->Void)? {
get {
return objc_getAssociatedObject(self, &AssociatedObject.key) as? ()->Void
}
set {
objc_setAssociatedObject(self, &AssociatedObject.key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
target = self
action = #selector(didTapButton(sender:))
}
}
#objc func didTapButton(sender: Any) {
actionClosure?()
}
}
extension UIViewController {
func addDissmissButton(){
let cancelButton = UIBarButtonItem.init(title: "Dismiss", style: .plain, target: self, action: nil)
cancelButton.actionClosure = {
self.dismiss(animated: true)
}
self.navigationItem.leftBarButtonItem = cancelButton
}
}
Is there a way to present a view controller modally without knowing what the visible view controller view is? Basically sort of like you would show an alert view at any points in time.
I would like to be able to do something like:
MyViewController *myVC = [[MyViewController alloc] init];
[myVC showModally];
I'd like to be able to call this from anywhere in the app, and have it appear on top. I don't want to care about what the current view controller is.
I plan to use this to show a login prompt. I don't want to use an alert view, and I also don't want to have login presentation code throughout the app.
Any thoughts on this? Or is there maybe a better way to achieve this? Should I just implement my own mechanism and just place a view on top of the window?
Well, you can follow the chain.
Start at [UIApplication sharedApplication].delegate.window.rootViewController.
At each view controller perform the following series of test.
If [viewController isKindOfClass:[UINavigationController class]], then proceed to [(UINavigationController *)viewController topViewController].
If [viewController isKindOfClass:[UITabBarController class]], then proceed to [(UITabBarController *)viewController selectedViewController].
If [viewController presentedViewController], then proceed to [viewController presentedViewController].
My solution in Swift (inspired by the gist of MartinMoizard)
extension UIViewController {
func presentViewControllerFromVisibleViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
if let navigationController = self as? UINavigationController {
navigationController.topViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else if let tabBarController = self as? UITabBarController {
tabBarController.selectedViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else if let presentedViewController = presentedViewController {
presentedViewController.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
} else {
present(viewControllerToPresent, animated: flag, completion: completion)
}
}
}
This solution gives you the top most view controller so that you can handle any special conditions before presenting from it. For example, maybe you want to present your view controller only if the top most view controller isn't a specific view controller.
extension UIApplication {
/// The top most view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
With this you can present your view controller from anywhere without needing to know what the top most view controller is
UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)
Or present your view controller only if the top most view controller isn't a specific view controller
if let topVC = UIApplication.topMostViewController, !(topVC is FullScreenAlertVC) {
topVC.present(viewController, animated: true, completion: nil)
}
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else if self is UIAlertController {
return nil
} else {
return self
}
}
}
You could have this code implemented in your app delegate:
AppDelegate.m
-(void)presentViewControllerFromVisibleController:(UIViewController *)toPresent
{
UIViewController *vc = self.window.rootViewController;
[vc presentViewController:toPresent animated:YES];
}
AppDelegate.h
-(void)presentViewControllerFromVisibleViewController:(UIViewController *)toPresent;
From Wherever
#import "AppDelegate.h"
...
AppDelegate *delegate = [UIApplication sharedApplication].delegate;
[delegate presentViewControllerFromVisibleViewController:myViewControllerToPresent];
In your delegate, you're getting the rootViewController of the window. This will always be visible- it's the 'parent' controller of everything.
I don't think you necessarily need to know which view controller is visible. You can get to the keyWindow of the application and add your modal view controller's view to the top of the list of views. Then you can make it work like the UIAlertView.
Interface file: MyModalViewController.h
#import <UIKit/UIKit.h>
#interface MyModalViewController : UIViewController
- (void) show;
#end
Implementation file: MyModalViewController.m
#import "MyModalViewController.h"
#implementation MyModalViewController
- (void) show {
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
// Configure the frame of your modal's view.
[window addSubview: self.view];
}
#end