What are proper places to detect foreground/background delegates in iOS? - ios

Backgound:
I am working in an iOS application. We have around 100 ViewControllers and all of them in our application are inherited from BaseViewController from the beginning. Currently while refactoring, I see many view controllers require to detect willEnterForegroundNotification[1] and didEnterBackgroundNotification[2]
delegates to do some internal tasks. Almost 20~25 view controllers are setting their own notification observers to the delegates on their viewDidLoad. I was thinking to move this detection task to central BaseViewController for code clarity.
My Proposed Solution:
My intended design is like below,
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToForeground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
}
func appMovedToBackground() {
print("App moved to Background!")
}
func appMovedToForeground() {
print("App moved to ForeGround!")
}
}
class MyViewController: BaseViewController {
override func appMovedToBackground() {
print(“Do whatever need to do for current view controllers on appMovedToBackground”)
}
override func appMovedToForeground() {
print(“Do whatever need to do for current view controllers on appMovedToForeground”)
}
}
I see that if I move this detection into BaseViewController many tasks of custom observer handling are reduced from child view controllers. Child ViewControllers (i.e. MyViewController in example code) only need to use these two functions appMovedToBackground and appMovedToForeground when they require.
Issues:
However, I am still concern about one thing. As I am moving the observer setting part into BaseViewController, thus all the ViewControllers (approx 100 of them in my project) will register the observer in their default viewDidLoad and many of them won’t even use them in reality. I am afraid this design might heavily costs app performance. Is my intended design acceptable when trading of between performance vs code clarity and maintainability in such situation? Is there any better design in my case?
Reference:
[1] willEnterForegroundNotification - Posted when the app enters the background.
[2] didEnterBackgroundNotification - Posted shortly before an app leaves the background state on its way to becoming the active app.

You can declare a protocol lets call it BGFGObserver.
Let each VC which needs to observe for foreground, background confirm to this protocol.
In base class check if self confirms to BGFGObserver, if yes then only register as observer.
In BGFGObserver you will need to have the methods to handle background and foreground.

Notification is one to many communication. If you really don't this functionality. you can use the protocol delegate method. you can assign a delegate, only when you need it.
and to solve your problem, you can move your observer to didSet of delegate variable. So, only when you assign a delegate, that time only observers will be added. if you don't set it, it will not be added for that viewController.
#objc protocol AppActivityTracker{
func appMovedToBackground()
func appMovedToForeground()
}
class BaseViewController: UIViewController {
var activityDelegate : AppActivityTracker? {
didSet{
//MARK:-Observer will be added only when you assign delegate.
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(activityDelegate?.appMovedToForeground), name: UIApplication.didBecomeActiveNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(activityDelegate?.appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
class MyViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
//MARK:- Assign delegate only when you need observers
activityDelegate = self
}
}
//MARK:- Assign delegate only when you need observers
extension MyViewController : AppActivityTracker{
func appMovedToBackground() {
print("Do whatever need to do for current view controllers on appMovedToBackground")
}
func appMovedToForeground() {
print("Do whatever need to do for current view controllers on appMovedToForeground")
}
}
Discussion from comment:-
Adding observer on viewWillAppear and removing observer on viewDidDisappear.
#objc protocol AppActivityTracker{
func appMovedToBackground()
func appMovedToForeground()
}
class BaseViewController: UIViewController {
var activityDelegate : AppActivityTracker? {
didSet{
if activityDelegate != nil{
addOberservers()
}
else{
removeOberservers()
}
}
}
func addOberservers(){
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(activityDelegate?.appMovedToForeground), name: UIApplication.didBecomeActiveNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(activityDelegate?.appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
func removeOberservers(){
let notificationCenter = NotificationCenter.default
notificationCenter.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
notificationCenter.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}
class MyViewController: BaseViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//MARK:- Assign delegate only when you need observers
self.activityDelegate = self
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
//MARK:- Removing observer on view will disappear.
self.activityDelegate = nil
}
}
//MARK:- Assign delegate only when you need observers
extension MyViewController : AppActivityTracker{
func appMovedToBackground() {
print("Do whatever need to do for current view controllers on appMovedToBackground")
}
func appMovedToForeground() {
print("Do whatever need to do for current view controllers on appMovedToForeground")
}
}

You can do this in AppDelegate or create a separate class that is held in AppDelegate to specifically do this.

Related

Applying same logic on all the ViewControllers

The following implementation works on a single ViewController. However, I want to apply same logic all the other ViewControllers as well.
Rather than repeating(copy-paste) the same code again and again in each of the ViewController, what would be a good approach?
ViewControllerA
override func viewDidLoad() {
super.viewDidLoad()
//common
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func willEnterForeground() {
if (expired()){
navigationController?.popToRootViewController(animated: true)
}
}
Subclasses. That's what subclasses are for. This absolutely should not be applied to all UIViewControllers, since many of those are provided by Apple, and it would be very bad if you modified their viewDidLoad this way. But for every one of your view controllers, you just need to add this behavior as a superclass.
To create a subclass like this, you'd make an intermediate view controller type:
class ForegroundPoppingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func willEnterForeground() {
if (expired()){
navigationController?.popToRootViewController(animated: true)
}
}
}
And then for all the view controller you want to have this behavior, you would subclass:
class MyViewController: ForegroundPoppingViewController { ... }
If MyViewController has its own viewDidLoad, you'd chain that to its superclass, just like in any other subclass:
class MyViewController: ForegroundPoppingViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ... any other behaviors ...
}
}
This would not apply to Apple view controllers, such as UIDocumentBrowserViewController, but it shouldn't. That may not give valid behavior. You would need to decide on the proper behavior depending on what view controller you're presenting.
Also, as a general rule, you should observe notifications in viewDidAppear (or willAppear) and remove notification observations in viewWillDisappear (or didDisappear). You usually do not want notifications firing on view controllers that are not currently onscreen.
That said, for this particular problem, I probably would recommend moving this logic to a "presenter" type coordinator, or even a UINavigationController subclass. As written, this may call popToRootViewController many times (since many view controllers may exist at the same time), which may lead to animation glitches.
But for the general question of how to add functionality to view controllers, this is how you would do it.

UITabBar Lifecycle's methods are not fired from background start

I've have a UITabBar controller as main controller, with 2 tabs. Each tab is a NavigatorViewController with a UIViewController embedded.
If I open the application from background after a previous cold launch, none of the ViewWillAppear (UITabBarController, UIViewController) is fired.
How can I call the lifecycle of UITabBarChildren when user come from backgroud? (IE: From a notification)
That is not in the life cycle because the state of controllers is not changing during background mode or other application events.
You should observe for applicationWillEnterForegroundNotification
class VC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Listen for application event somewhere early like `ViewDidLoad`
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
}
// Implement a function that you want to execute when event happen
#objc func applicationWillEnterForegroundNotification() {
// Do anything before application Enter Foreground
}
// Remove observer when the controller is going to remove to prevent further issues
deinit {
NotificationCenter.default.removeObserver(self)
}
}
When application comes from background non viewWillAppear/viewDidAppear is called for any active vc , you need to listen to app delegate like applicationWillEnterForegroundNotification
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
#objc func applicationWillEnterForegroundNotification(_ notification: NSNotification) {
print("To-Do")
}
You can add an observer to UIApplicationWillEnterForeground in your controllers.
Posted shortly before an app leaves the background state on its way to
becoming the active app.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,selector: #selector(self.appEnteredFromBackground(_:)),name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
}
#objc func appEnteredFromBackground(_ notification: NSNotification) {
print("From background")
}

Where to implement applicationWillResignActive(_:)?

In order for the ViewController to "do something specific" just before the app goes into the background state...
I am to understand that this sort of thing is generally handled inside the
applicationWillResignActive(_:) method, but this method resides inside the AppDelegate class, not the ViewController.
This is my first time doing lifecycle related stuff on IOS, and so I'm not sure whether to:
1) Call a ViewController method from inside the AppDelegate class. This would mean that I have to change the method from private to public.
2) Have the ViewController implement UIApplicationDelegate
PS - Is it okay to just delete the AppDelegate class as long as the ViewController implements UIApplication delegate instead?
EDIT: I should add that this is a single-page app with only one view controller (well, I suppose it will have a settings view controller eventually... but the 'ViewController' that I am referring to will never be popped off the stack).
Thanks!
Generally you shouldn't delete the AppDelegate unless you have a really good reason. This isn't a good reason.
For your scenario I would investigate using NotificationCenter to observe the UIApplicationWillResignActive event. This event is fired every time the application will enter the background.
For more information see: Apple Docs
e.g.
func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated: animated)
NotificationCenter.default.addObserver(self, selector: #selector(youFunction), name: .UIApplicationWillResignActive, object: nil)
}
func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated: animated)
NotificationCenter.default.removeObserver(self)
}
Use NotificationCenter
In YourViewController
class YourViewController : UIViewController {
override func viewDidAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(reloadTableData(_:)), name: .UIApplicationWillResignActive, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .UIApplicationWillResignActive, object: nil)
}
}
func reloadTableData(_ notification: Notification) {
}

Swift delegates isn't working

I'm trying to use delegates between two controllers but it doesn't work as it should be
protocol saveDelegate: class {
func saveSite()
}
class AuditSiteViewController: UIViewController {
weak var delegate: saveDelegate?
#IBAction func saveButton(sender: UIBarButtonItem) {
print("Saved")
delegate?.saveSite()
}
}
class AuditDetailsViewController: UIViewController, saveDelegate {
var mainView: AuditSiteViewController?
override func viewDidLoad() {
super.viewDidLoad()
mainView?.delegate = self
}
func saveSite() {
print("delegated")
}
}
it should print delegated but it only prints "saved"?
You can use delegate, but have you debug and check that mainView is the correct instance?
My suggestion in this case would be to use NSNotification instead. You can add a observer in your viewDidLoad and post a notification on the saveButton()
class AuditDetailsViewController: UIViewController {
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(AuditDetailsViewController.saveSite), name: "SaveSite", object: nil)
}
}
class AuditSiteViewController: UIViewController {
#IBAction func saveButton(sender: UIBarButtonItem) {
NSNotificationCenter.defaultCenter().postNotificationName("SaveSite", object: nil)
}
}
In my opinion there are only two reasons possible:
First:
In the moment of calling mainView?.delegate = self mainView is nil. Then the delegate isn't assigned. Set a breakpoint there and you will see it.
Second:
In the moment of calling delegate?.saveSite() the delegate is nil. That may be because your instance of AuditDetailsViewController was deinit by you or system. System removes the instance if noone holds a strong reference to it anymore. Implement the deinit method and set a breakpoint in it to see when it happens.
Looks like the mainView is nil when you set the delegate. Try to set the reference when you instantiate the detail view controller.
Anyway, maybe what you want is to delegate the saving action from the detailViewController to the AuditSiteViewController and handle in this last VC the savings.

IOS Swift handle global events

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

Resources