How to update UI from background state - ios

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.

Related

View Controller1 in a TabBarController won't automatically update view after ViewController2 is dismissed

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.

Which View Controller is active when app comes to foreground?

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.

where does back event is handel in swift 3.0

I want to refresh the whole page controller on back press.
I am navigating the viewcontroller using code.
My Code
let GTC = self.storyboard?.instantiateViewController(withIdentifier: "GoToCart")as! GoToCart
self.navigationController?.pushViewController(GTC, animated: true)
Using viewWillAppear to reload your UI. As you use navigationController?.pushViewController, the view will be retained and stored in stack.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Reload the UI
}
viewWillAppear(_:)
viewWillAppear is called the first time the view is displayed as well as when the view is displayed again, so it can be called multiple times during the life of the view controller object. It’s called when the view is about to appear as a result of the user tapping the back button, when the view controller’s tab is selected in a tab bar controller etc. Make sure to call super.viewWillAppear() at some point in the implementation. You can refresh your UI in this method
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Reload the UI
}
A better approach is to use protocol
Create protocol from where you want to pop back(GoToCart)
Create delegate variable in GoToCart
Extend GoToCart protocol in MainViewController
Give reference to GoToCart of MainViewController when
navigate
Define delegate Method in MainViewController
Then you can call delegate method from GoToCart
Example
In GoToCart: Write code below..
protocol GoCartControllerDelegate
{
func childViewControllerResponse(parameter)
}
class GoToCart:UIViewController
{
var delegate: ChildViewControllerDelegate?
....
}
Then in mainViewController implement the protocol function end extend to the protocol
class MainViewController:UIViewController,GoCartControllerDelegate
{
// Define Delegate Method
func childViewControllerResponse(parameter)
{
//...here update what you want to update according to the situation
}
}
2 Important thing
when navigating to the gocart controller code like this
let GTC = self.storyboard?.instantiateViewController(withIdentifier: "GoToCart")as! GoToCart
GTC.delegate = self
self.navigationController?.pushViewController(GTC, animated: true)
and when popping from gocartViewController
code like this
self.navigationController?.popViewController(animated:true)
self.delegate?.childViewControllerResponse(parameter)

inputAccessoryView disappears after presenting UIViewController

I have a UICollectionViewController with an inputAccessoryView. Everything works great until I present a UIViewController, and then the accessory view disappears. Trying to get basic Chat application features.
I have implemented in the collection view:
override var inputAccessoryView: UIView? {
get {
return inputContainerView
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func becomeFirstResponder() -> Bool {
return true
}
As suggested in multiple other threads, I also call (in the collection view),
view.resignFirstResponder()
view.inputAccessoryView?.reloadInputViews()
view.becomeFirstResponder()
after dismissing the UIViewController but to no avail. print(view.isFirstResponder) still prints false. I have tried almost every combination of the above three lines in numerous different places in my code. I think I'm missing something simple.
The animation to present and dismiss view controller might be causing issue or You haven't maintained a global ivar for the view you have setted as input accessory view. Try creating a readonly ivar for the accessoryview so only one instance is allocated and maintained through out VC life cycle. Then ensure to set it back to the textfields before calling reloadInputViews.
After a few days I finally figured out something that works... I think I was trying to present the loginController before the collectionView was set as the first responder. Instead of just calling present I called this function:
func presentLoginControllerAfterImFirstResponder(fromUser: Bool) {
// Starts a timer
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { (timer) in
// Is this view the first responder?
if self.isFirstResponder {
// Creates the loginController
let loginController = LoginController()
// Presents it
self.present(loginController, animated: fromUser, completion: {
// Once presented, sets rootViewController = self
loginController.rootViewController = self
})
// Stop the timer
timer.invalidate()
}
}
}
This works now. I guess the collectionView needed some time to set itself as the first responder? I can present and dismiss the loginController no problem and the inputAccessoryView remains.

iOS equivalent for onRestart()

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)
}
}

Resources