Normally I can find out when a View Controller appears with
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
This won't be called though if the user presses the home button or for some other reason the app goes to the background and then returns to the foreground. To find out when the app comes to the foreground I can add an observer to the Notification Center.
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("FirstViewController")
}
#objc func appWillEnterForeground() {
print("app in foreground")
}
}
However, my problem is that I have a tabbed app I want to know which View Controller is active when the app comes back into the foreground. The Notification center just sends a general message. Even though I an setting the notification observer in the first tab, it could be on any tab when the app goes into the background.
NSNotification.Name.UIApplicationWillEnterForeground
is a notification thrown by Notification Center. So obviously that is not related to any specific VC. What you can do rather is,
#objc func appWillEnterForeground() {
if self.viewIfLoaded?.window != nil {
// viewController is visible
}
}
Though notification of App entering foreground gets triggered to every viewController observing it, only the VC which is currently loaded and visible will have its code in if condition executed. That gives you a control to decide which VC is currently visible.
EDIT 1:
All that you want to figure out is the top ViewController in navigation stack of TabBarControllerwhen app comes to foreGround, you can add the observer for NSNotification.Name.UIApplicationWillEnterForeground only in UITabBarControllerand in
#objc func appWillEnterForeground() {
var vc : UIViewController = tabBarController.viewControllers![tabBarController.selectedIndex]
while vc.presentedViewController != nil || self.childViewControllers.count != 0 {
if vc.presentedViewController != nil {
vc = vc.presentedViewController!
}
else {
vc = vc.childViewControllers.last!
}
}
print("\(vc) should be the top most vc")
}
Use the Notification Observer and your appWillEnterForeground() or any event which gets fired from the observer in all view controllers under tab controller. So whichever the view controller you came back to will get your notification event get triggered in that particular VC. If you're looking for a centralized solution, this scattered gun approach may not work.
Related
I'm trying to create a timer app. I have a singleton class with a Timer which fires every x minutes. Using custom delegate I pass the data to active view controller and update the value in a label. If the data is when the count is y, I perform push and update the count in another view controller's label.
When application is in foreground I didn't get any problem. If the application is in background state the counter keeps running and label text isn't updated and push isn't performed. Still I'm in first view controller. How to solve this?
like I mentioned in your comment section. when you app is in the background state you shouldn't continuously updating your UI as it is pointless. when user tapped back into your app your view controller will call a function viewDidAppear(animated). In that function you can check timer condition then present second view controller if needed. I'll post a sample code below
class FirstViewController : UIViewController {
var timerExpiration = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if timerExpiration {
let vc = SecondViewController()
present(vc, animated: true, completion: nil)
}
}
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
You need to wrap all foreground UI tasks in a block like this
DispatchQueue.main.async {
// do your UI stuff here, like
// label.text = "Main thread stuff"
}
Put this in your timer action.
I have values that are being updated from firebase, I am calling the functions that update the values inside viewdidload(), when the app is installed the values are updated, then I send an update that is basically requiring me to close the app twice until the value are update, not sure if iphone keeps it in memory even after 1 close?
I tried adding viewWillAppear and viewDidAppear to try and fix this but neither did anything. Is there a way to reload the view when the app is opened even if it was not closed and was in the background.
Will this work through app delegate? how can i Update the viewcontroller from there?
Thank you.
It is better to listen for UIApplication.didBecomeActiveNotification The iOS sends this notification when the app goes from background to foreground.
You can install handler for that notification for example in viewDidLoad method of your controller.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(handleAppDidBecomeActiveNotification(notification:)),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}
Your handler will be just a method in the same view controller to call when receive this notification. In that method you can reload the data or do anything you'd like
#objc func handleAppDidBecomeActiveNotification(notification: Notification) {
reloadData()
}
And of course do not forget to unregister for UIApplication.didBecomeActiveNotification when the view controller is closed. For example in the deinit method of the controller
deinit {
NotificationCenter.default.removeObserver(self)
}
In that way the logic for reload and what triggers the reload will be capsulated in each view controller and the app delegate will not know about this which is better.
You could handle it from AppDelegate.
Use optional application did become active method.
func applicationDidBecomeActive(_ application: UIApplication)
This method is called to let your app know that it moved from the inactive to active state. You should use this method to restart any tasks that were paused (or not yet started) while the app was inactive.
Edited to show how to get desired controller.
Check controller inside it:
func applicationDidBecomeActive(_ application: UIApplication) {
let appDelegate: AppDelegate? = UIApplication.shared.delegate as? AppDelegate
if let controller = appDelegate?.window?.rootViewController {
if let navigationController: UINavigationController = controller as? UINavigationController {
let viewControllers: [UIViewController] = navigationController.viewControllers
for viewController in viewControllers {
// Check for your view controller here
}
} else if let viewController: UIViewController = controller as? UIViewController {
// Check for your view controller here
} else if let tabController: UITabBarController = controller as? UITabBarController {
// Narrow the hierarchy and check for your view controller here
}
}
}
You have to call method tableView.reloadData() in the viewDidLoad() after loading data from firebase.
For instance have a look on my example:
// no matter what you have collectionView or tableView
#IBOutlet private weak var cardsCollectionView: UICollectionView!
// data from firebase stored in the array
private var cards: [Card]?
override func viewDidLoad() {
super.viewDidLoad()
DatabaseService.shared.loadDataFromDb { (cards) in
DispatchQueue.main.async {
// update of array with new data
self.cards = cards
// update of view
self.cardsCollectionView.reloadData()
}
}
}
Say you have
var someVC: UIViewController
is it possible to essentially do the following, somehow?
get a notification when {
someVC has a viewWillAppear
self.#selector(wow)
}
#objc func wow() {
print("we spied on that view controller, and it just willAppeared"
}
Is that possible ?
(Or maybe on didLayoutSubviews ?)
(I realize, obviously, you can do this by adding a line of code to the UIViewController in question. That's obvious. I'm asking if we can "add on" to it from elsewhere.)
If I understand your question correctly, you want ViewController B to receive a notification when viewWillAppear is called in ViewController A? You could do this through the Notifications framework. Keep in mind that both VC's have to be loaded for one to receive a notification.
Alternatively, if the two VC's are on the screen at the same time, then I'd recommend a delegate pattern - have VC A tell an overarcing controller class that it's viewWillAppear has been called, and this overarcing controller will then inform ViewController B.
To do this using Notifications:
(This is from memory, so please excuse typos)
class TestClassA: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// To improve this code, you'd pull out the Notification name and perhaps put it into an extension, instead of hardcoding it here and elsewhere.
NotificationCenter.default.post(Notification.init(name: Notification.Name.init(rawValue: "viewControllerAppeared")))
}
}
class TestClassB: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(viewControllerAppeared(notification:)), name: Notification.Name.init(rawValue: "viewControllerAppeared"), object: nil)
}
#objc func viewControllerAppeared(notification: NSNotification) {
print("other viewcontroller appeared")
}
}
Documentation
What is the iOS equivalent for onRestart() used on Android?
onRestart() is called when current activity is being re-displayed to the user (the user has navigated back to it).
I believe you need viewWillAppear method:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//your code here
}
It is called every time right before view is going to be shown. So it will be called when view is shown for the first time as well.
If you want to avoid running your code for the first time viewWillAppear is called, you will have to add a flag property and check if it has been set previously.
If you're trying to capture whenever the scene in question comes into view, there are two cases you might be concerned about:
If you transition to this scene (or dismissing/popping back to this scene) from within the app. In that case, use viewWillAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
update() // your routine for updating what is displayed to the user
}
If your app is running and you press the "home" button (or go to another app), and then later return to the your app (before it is terminated), viewDidAppear is not called. To detect that scenario, you can observe .UIApplicationDidBecomeActive:
private var observer: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: .UIApplicationDidBecomeActive, object: nil, queue: nil) { [weak self] notification in
self?.update()
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
How can I handle global events triggered by the notification centre for example in my API class I fire an event if an error response is received e.g. (500). When that event is fired an UIAlert should be displayed on what ever view controller is active, or on logout the login view controller should be presented.
As far as I can see there is no easy way to get the current view controller in order to interact with it. (Note that my root view controller is NOT a navigation controller).
An alternative solution, that will work regardless of whether your view controllers are embedded in a UINavigationController or not, would be to subclass UIViewController. This class will handle receiving the NSNotification that an error occurred and will also handle displaying the alert:
class MyViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "errorOccured",
name: "ErrorNotification",
object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "ErrorNotification", object: nil)
}
func errorOccured() {
// Present an UIAlertViewController or the login screen.
}
}
Now, any UIViewControllers that should display an alert when the error notification is posted just have to be a subclass of MyViewController. Just make sure, if you override viewWillAppear or viewWillDisappear, that you call super.viewWillAppear or super.viewWillDisappear.
Is this way too hard to get current view controller ( when not using navigation controller ) ?
// on your app delegate
getCurrentViewController(self.window!.rootViewController!)
func getCurrentViewController(viewController:UIViewController)-> UIViewController{
if let navigationController = viewController as? UINavigationController{
return getCurrentViewController(navigationController.visibleViewController)
}
if let viewController = viewController?.presentedViewController {
return getCurrentViewController(viewController)
}else{
return viewController
}
}
For BroadCast Notification
NSNotificationCenter.defaultCenter().postNotificationName("erro400", object: nil)
For Receive
override func viewWillAppear(animated: Bool) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "ErroOccure", name: "erro400", object: nil)
}
func ErroOccure()
{
//present alert from here
// do whatever you want
}
You have to Remove Notification when you finish with it.
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self)
}