How to check in AppDelegate if a particular ViewController is currently open - ios

I am trying to prevent a push notification show on the app home screen when a certain userMessagesViewController is currently open.
I don't want users receiving a push notification if this specific viewController is open. My function that sends the push notification is in the appDelegate. How can I check. Here is my implementation so far.
let messagesVC = UserMessageViewController()
if messagesVC.view.window != nil {
print("Messages viewcontroller is visible and open")
} else {
print("Messages viewcontroller isnt visible and not open")
}

By initiating messagesVC, you're creating a brand new UserMessageViewController that won't have been presented yet. The particular instance of the controller you want will already be instantiated, so you must find it using the view controller hierarchy.
The AppDelegate gives you access to the rootViewController of your application which will be the very first controller you have in your storyboard. From this controller, you can make your way through the child view controllers in search of a UserMessageViewController.
Here is an extension that will start at the rootViewController and bubble its way up until it reaches the top of the view controller hierarchy stack.
extension UIApplication {
func topViewController(_ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
switch (base) {
case let controller as UINavigationController:
return topViewController(controller.visibleViewController)
case let controller as UITabBarController:
return controller.selectedViewController.flatMap { topViewController($0) } ?? base
default:
return base?.presentedViewController.flatMap { topViewController($0) } ?? base
}
}
}
Create a new file called UIApplication+TopViewController.swift and paste in the above extension. Then inside AppDelegate, you will be able to get the current view controller that is being presented using UIApplication.shared.topViewController():
if let messagesVC = UIApplication.shared.topViewController() as? UserMessageViewController {
print("Messages viewcontroller is visible and open")
} else {
print("Messages viewcontroller isnt visible and not open")
}
By casting the top view controller to UserMessageViewController, we can determine whether or not the notification should be presented.

This should work for you:
if messagesVC.viewIfLoaded?.window != nil {
// viewController is visible, handle notification silently.
}
Your appDelegate will have a reference to the VC. It should probably be a property of the delegate.

Related

present a ViewController from a Swift class derived from an NSObject?

This project was written in Objective C and a bridging header and Swift files were added so they can be used at run time. When the app starts, Initializer called in Mock API client is printed in the debugger. Is it possible to present a ViewController from the initializer?
Xcode Error:
Value of type 'MockApiClient' has no member 'present'
//MockApiclient.Swift
import Foundation
class MockApiClient: NSObject
{
override init ()
{
print("Initializer called in Mock API client")
if isLevelOneCompleted == false
{
print("It's false")
let yourVC = ViewController()
self.present(yourVC, animated: true, completion: nil)
} else
{
print("It's true")
}
}
var isLevelOneCompleted = false
#objc func executeRequest()
{
print("The execute request has been called")
isLevelOneCompleted = true
if isLevelOneCompleted {
print("It's true")
} else {
//do this
}
}
}
Update - ViewController.m
// prints "The execute request has been called" from the debugger window
- (void)viewDidLoad {
[super viewDidLoad];
MockApiClient *client = [MockApiClient new];
[client executeRequest];
}
You can't call present(_:animated:completion) because it is a method of UIViewController, not NSObject.
Why not pass a viewController reference to the MockApiClient to present on instead like so. Be sure to check Leaks or Allocations on instruments to avoid the client retaining the controller.
class MockApiClient: NSObject {
var referencedViewController: UIViewController?
override init() {
let presentableViewController = ViewController()
referencedViewController.present(presentableViewController, animated: true, completion: nil)
}
deinit {
referencedViewController = nil
}
}
let apiClient = MockApiClient()
apiClient.referencedViewController = // The view controller you want to present on
Assuming you're using UIKit, you'll have to present the view controller from the nearest available attached view controller. If you know for certain that no other view controllers would currently be presented then you can safely present from the root view controller:
UIApplication.shared.keyWindow?.rootViewController?.present(someViewController, animated: true, completion: nil)
This concept of attached and unattached/detached view controllers is never officially explained but the infamous UIKit warning of presenting view controllers on detached view controllers is real. And the workaround is finding the nearest available attached view controller, which at first (when nothing is currently being presented) is the root view controller (of the window). To then present an additional view controller (while one is currently being presented), you'd have to present from that presented view controller or its nearest parent view controller if it has children (i.e. if you presented a navigation view controller).
If you subclass UIViewController, you can add this functionality into it to make life easier:
class CustomViewController: UIViewController {
var nearestAvailablePresenter: UIViewController {
if appDelegate.rootViewController.presentedViewController == nil {
return appDelegate.rootViewController
} else if let parent = parent {
return parent
} else {
return self
}
}
}
Then when you wish to present, you can simply do it through this computed property:
nearestAvailablePresenter.present(someViewController, animated: true, completion: nil)

Swift Deeplink to already open App's already open viewcontroller?

I have an app that accepts a Deeplink URL and opens a viewcontroller with variables from the link and it works well if the App is opened/run for the first time by the user using the Deeplink.
However, if the App is already open/or in the background and has that view controller open... it then opens the same viewcontroller back up again so then I have two. I do not want to open the viewcontroller an additional time.
Is there some way I can identify that viewcontroller that is already open and pass the variables from the Deeplink to it?
or do I need to close it in some way and re-open it?
I am open to suggestions.... thanks in advance.
Try using UIApplication.shared.keyWindow?.rootViewController and testing what class it is. For example:
if let vc = UIApplication.shared.keyWindow?.rootViewController {
if vc is SomeViewController {
// Do something.
}
}
You can find the visible view controller with the following method
func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {
var rootVC = rootViewController
if rootVC == nil {
rootVC = UIApplication.shared.keyWindow?.rootViewController
}
if rootVC?.presentedViewController == nil {
return rootVC
}
if let presented = rootVC?.presentedViewController {
if presented.isKind(of: UINavigationController.self) {
let navigationController = presented as! UINavigationController
return navigationController.viewControllers.last!
}
if presented.isKind(of: UITabBarController.self) {
let tabBarController = presented as! UITabBarController
return tabBarController.selectedViewController!
}
return getVisibleViewController(presented)
}
return nil
}
you can then switch on the presented view
if let presentedView = getVisibleViewController(window?.rootViewController) {
switch presentedView {
//code
default:
//code
}
}
and of course in the switch present a view controller if it is not the one that you want to be open.
No need to close a viewcontroller before opening it!

Dismissing a modal view but keeping the data

I'm trying to dismiss a modal view and return back to the view controller that was "sent" from, while keeping the data that was entered in the modal view. If I understand correctly I need to use delegates/protocols for this but I'm having a lot of trouble understanding how to actually implement it in this situation.
Basically a user can call a modal view to enter some information in text fields, and when they hit save this function is called:
func handleSave() {
guard let newProductUrl = NSURL(string: urlTextField.text!) else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
// Call save function in view controller to save new product to core data
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
// Present reloaded view controller with new product added
let cc = UINavigationController()
let pController = ProductController()
productController = pController
cc.viewControllers = [pController]
present(cc, animated: true, completion: nil)
}
Which calls the self.productController?.save function to save the newly entered values into core data, and reloads the productController table view with the new product.
However the issue I'm running into, is that the productController table view is dynamically set depending on some other factors, so I just want to dismiss the modal view once the user has entered the data, and return back to the page the modal view was called from.
EDIT: attempt at understanding how to implement the delegate -
ProductController is the parent class that the user gets to the modal view from:
protocol ProductControllerDelegate: class {
func getData(sender: ProductController)
}
class ProductController: UITableViewController, NSFetchedResultsControllerDelegate, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
weak var delegate:ProductControllerDelegate?
}
func getData(sender: ProductController) {
}
And AddProductController is the modally presented controller where the user enters in the data then handleSave is called and I want to dismiss and return to the ProductController tableview it was called from:
class AddProductController: UIViewController, ProductControllerDelegate {
override func viewDidDisappear(_ animated: Bool) {
// error on this line
getData(sender: productController)
}
If the sole purpose of your protocol is to return the final state of the view controller its usually easier and clearer to use an unwind segue instead of a protocol.
Steps:
1) In the parent VC you make a #IBAction unwind(segue: UIStoryboardSegue) method
2) In the storyboard of the presented ViewController you control drag from either the control you want to trigger the exit or from the yellow view controller itself(if performing the segue in code) on to the orange exit icon.
your code should look like:
#IBAction func unwind(segue: UIStoryboardSegue) {
if let source = segue.source as? MyModalViewController {
mydata = source.data
source.dismiss(animated: true, completion: nil)
}
}
see apple documentation
Edit here is the hacky way to trigger and unwind from code without storyboard; I do not endorse doing this:
guard let navigationController = navigationController,
let presenter = navigationController.viewControllers[navigationController.viewControllers.count - 2] as? MyParentViewController else {
return
}
presenter.unwind(UIStoryboardSegue(identifier: String(describing: self), source: self, destination: presenter))
Basically you need to create a delegate into this modal view.
Let's say you have ParentViewController which creates this Modal View Controller. ParentViewController must implement the delegate method, let´s say retrieveData(someData).
On the Modal View Controller, you can use the method viewWillDisappear() to trigger the delegate method which the data you want to pass to the parent:
delegate.retrieveData(someData)
If you have issues understanding how to implement a delegate you can check this link

Segue to a view controller from appDelegate

I have an app set up to receive push notifications. When a user receives a notification, I have a callback in my appDelegate. I need to be able to segue from here if the app was inactive and the user clicks on the notification from the panel on the device.
The flow of the app is a login view controller (which gets skipped if the loginBool is true) which leads to a tab controller. On the tab controller I have 3 places where I can segue to the same viewController with an id of "FeedDetailedController".
It is the FeedDetailedController I need to segue to and pass in a variable which I am receiving in my notification. This controller can be accessed from 3 different places 2 of which are tabs with table views, when you click on a row, it passes in a variable and performs a segue. I need to miic this from my app delegate i.e. pass in data from the notification, like what I am doing with the row.
Attempt so far:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
println("received a notification")
PFPush.handlePush(userInfo)
if application.applicationState == UIApplicationState.Inactive {
println("in the notification if with \(userInfo)")
if let info = userInfo["custom"] as? Dictionary<String, AnyObject> {
if let reportId = info["reportId"] as? String {
println("\nFrom APS-dictionary with key \"type\": \(reportId)")
//pass in reportId to the viewcontroller somehow
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("NewFeedDetailedController") as! UIViewController
let navigationController = UINavigationController(rootViewController: vc)
self.window?.rootViewController?.presentViewController(navigationController, animated: true, completion: nil)
}
}
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo)
}
else{
println("in the notification else")
//this is when the app is active, do I need to detect which view controller I am currently on before I can seg???
}
}
The current code gives the following message:
Warning: Attempt to present <UINavigationController: 0x12ed763d0> on <UINavigationController: 0x12ed11ce0> whose view is not in the window hierarchy!
Which makes sense but I don't know how I am supposed to get the hierarchy right coming from appDelegate code
Would it be possible to check for that notification in the initial view controller of your app? Like pass it the notification as a boolean? You could look somewhere like viewWillAppear: and check for it - if it is there control the segue from that view instead of trying to do it in the appDelegate itself?
That way it would appear to go directly into the new view, but really be passing through a view controller that has the hierarchy loaded into it?

Call ViewControllers Method or Segue from App Delegate (Swift)

I have created an app that uses storyboard and have successfully created a tableview and detail page which all works.
I would like it so that users swiping the localNotifications can be sent to the correct detail page within the app.
It appears that I can call functions from the ViewController but whenever they refer to themselves to update any details or perform a segue the app crashes.
The code i have in my ViewController is as follows:
func handleMessageNotification (messageID:String)
{
// this function should act as a conduit for the call from the delegate
println("notif messageID: \(messageID)");
self.performSegueWithIdentifier("showMessageDetail", sender: self);
self.messageView.reloadData();
}
and this is called from my appDelegate
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
if (application.applicationState == UIApplicationState.Background || application.applicationState == UIApplicationState.Inactive)
{
var rootViewController = self.window!.rootViewController
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as ViewController
rootViewController?.navigationController?.popToViewController(setViewController, animated: false)
setViewController.handleMessageNotification(messageID);
}
}
the println works correctly but performSegue fails (fatal error: unexpectedly found nil while unwrapping an Optional value) and the messageView.reload() fails (same error).
How do I get a notification to fire the app to the correct place upon opening?
This solution 'Get Instance Of ViewController From AppDelegate In Swift' uses much of the same but mine will not allow access to anything with the ViewController.
======= Update - Solution =======
For anyone else with this issue. Following on from what Gabuh had suggested below; the full solution for me was to do the following:
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
if (application.applicationState == UIApplicationState.Background || application.applicationState == UIApplicationState.Inactive)
{
let navigationController = application.windows[0].rootViewController as UINavigationController;
navigationController.popToRootViewControllerAnimated(false); // I need to push back to root before performing segue
let rootViewController = navigationController.visibleViewController;
rootViewController.performSegueWithIdentifier("showMessageDetail", sender: self);
}
}
this also allows me to make calls on functions in the view such as in the original example e.g.
rootViewController.handleMessageNotificaion(messageID);
or
rootViewController.messageView.reloadData();
You are creating an instance of a new ViewController. And you are trying to call a function on that ViewController, that is trying to perform a segue, when the ViewController is not even displayed. I think that's not going to work...
If you want to perform a segue from your detail view, you have to access to the current instantiated view Controller and perform the segue from it.
If you want to know how to get the instance of the UIViewController that is being displayed, in this post there are several ways that show you how to do it:
Get the current view controller from the app delegate
In pseudo-code, in your appDelegate:
1- get current UIViewController instance (not new one)
2- perfom segue from that view controller (or present modally or whatever transition you want)
In Swift 3:
guard let rvc = self.window?.rootViewController as? VCName else { return }
rvc.methodInYourVC()
The above assumes
That you don't want to create a whole new instance of VCName, just want a reference to it
That you know the VC you want to reference is your root VC

Resources