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()
}
}
}
Related
SCENARIO
Xcode 11.5, Swift 5
Using Core Data
User wants to update their profile. VC2 is dismissed after user taps save. VC1 area highlighted in yellow should reflect the change.
PROBLEM
Data is being saved correctly. However, VC1 elements highlighted in yellow doesn't automatically update. If I go to another tab then come back, the view elements refresh with the updated changes.
MY CODE
I have a setupUI() method that lays out the elements and have tried adding it to VC1's viewWillAppear method, but no luck.
//VC1:
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
setupUI()
}
viewWillAppear is not called when you dimiss the modal that fill the data you need to use a delegate
1- When you show the modal vc
let vc = SomeVC()
vc.delegate = self // declare property delegate inside the modal of that type / protocol
// present
2- when you dimiss the modal
self.delegate?.setupUI()
// dimiss
You could use a delegate method to perform some changes in VC1 in response to some action in VC2. In this case you will set the delegate in VC1 and call the delegate method in VC2. Ideal place to make this call would be in completion block of dismiss.
//VC1
public protocol MyProtocol: class {
func delegateMethod()
}
In the viewDidLoad method set the delegate for VC2
override func viewDidLoad() {
super.viewDidLoad()
fetchUser()
setupUI()
//VC2 is the instance of view controller you are going to push from this page
VC2.delegate = self
}
Make sure VC1 confirms to MyProtocol protocol
extension VC1: MyProtocol {
func delegateMethod() {
// reload view here
}
}
Declare the delegate in VC2
//VC2
var delegate: MyProtocol?
Then call the delegate method in completion of dismiss
self.dismiss(animated: false, completion: {
self.delegate?.delegateMethod()
})
Alternatively you could use observers to respond to any changes as well, but that might be an overkill. Check out this article, they discuss the whole thing in detail.
setting vc2.modalPresentationStyle = .fullScreen will solve it without the need to make any delegates.
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.
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.
My Xcode debugger reports, every other values include alive (bool value) is set, except all the IBOutlets found nil (myLine, etc.). By the way, everything works when I delete all the code in App Delegate, but I need this view to update frequently, so implement it in applicationWillEnterForeground is necessary. And another thing worth pointing out, in the configure view 1 and 2, I set each outlet's value. And I make app delegate call viewdidload method before that, so all the outlets should hooked up with code already, so those outlets shouldn't be nil.
An error message in the debugger -- fatal error: unexpectedly found nil
while unwrapping an Optional value
import UIKit
class ViewController: UIViewController {
var alive : Bool = true
var aliveDefault = NSUserDefaults.standardUserDefaults()
//IBOulet Connections
#IBOutlet weak var myLine: UILabel!
#IBAction func buttontapped(sender: AnyObject) {
alive = false
NSUserDefaults.standardUserDefaults().setObject(alive, forKey: "alive")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
loadView()
}
func loadView(){
alive = aliveDefault.boolForKey("alive")
if alive = true {
configureView()
}else{
configureView2()
}
}
func configureView(){
myLine.text = "Random text"
}
func configureView2(){
myLine.text = "Random text 2"
}
}
App Delegate
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
ViewController().viewDidLoad()
ViewController().configureView()
}
Since you are creating two new instances of ViewController in applicationWillEnterForeground using the default initialiser rather than from the storyboard, none of the IBOutlets will be set. You need to update the current instance of the view controller that is on screen.
Rather than doing this from the appDelegate it is probably easier to have your view controller subscribe to the UIApplicationWillEnterForegroundNotification NSNotification and handle the refresh locally. This way you don't need to closely couple your app delegate and your view controller and you don't need to worry if the view controller isn't currently on screen:
class ViewController: UIViewController {
...
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.loadView), name: UIApplicationWillEnterForegroundNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Do ViewController.loadViewIfNeeded() before assigning values to IBOutlets
I'm working on an app that fetch data from a website. When the user hit the home button then open the app again (from background), I want to reload the data again to the viewController.
I tried the following code:
in app delegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var myViewController: ViewController?
---------
var myViewController: rootViewController?
func applicationDidEnterBackground(application: UIApplication) {
print("Goodbye world") //.... then whatever code after pressing the home button
}
func applicationWillEnterForeground(application: UIApplication) {
print("Hello World")
myViewController.ObtianData() // which is pretty much the func in my app that fetch data from the web and display it in tableView
}
Then in the ViewController under ViewDidLoad
override func viewDidLoad() {
// I added the print to log here to check if the viewDidLoad function is being called but apparently it is not.
print ("Hello again from ViewController")
let appDelegate:AppDelegate = UIApplication.sharedApplication().delegate! as! AppDelegate
appDelegate.myViewController? = self
}
Any suggestions?
The root cause of the problem that you are having is that the data that you load is attached to the view controller that displays it. This goes against the MVC principle, which suggests that the model needs to be separated from the controller.
You should reorganize the classes in such a way that ObtainData is split between the model and the controller:
The model goes out and obtains the data,
The controller decides what to do with the data.
Make a class called Model (or pick some other name with Model in it) and store the data for your table in it. Make a single instance of that class statically accessible from everywhere through Model.instance (i.e. implement a Singleton in Swift).
Change your view controller to rely on Model.instance for its data, rather than storing it internally.
That is all you need to do to separate the pieces of your app. Now your problem can be solved in exactly two lines of code - applicationWillEnterForeground should call Model.instance.obtainData, and your controller's viewWillAppear should call reloadData on its tableView.
You should use NSNotificationCenter event UIApplicationDidBecomeActiveNotification, it was made especially for that.
(You don't need to use AppDelegate)
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(applicationDidBecomeActive),
name: UIApplicationDidBecomeActiveNotification,
object: nil)
}
func obtianData() {
// do something
}
Note that swift standard require function name to start with a lower case.
add Notification with UIApplicationDidEnterBackgroundNotification and UIApplicationDidEnterBackgroundNotification