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.
Related
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.
I have an app that I am developing using Swift 4.0. I have a View Controller on which I am showing some useful information and user can interact with them. Lets call this as BaseViewController.
What I am doing:
The BaseViewController is starting different other ViewControllers, and than user can dismiss those viewControllers and come back to BaseViewController.
What I want:
Now I want that whenever user comes back to BaseViewController it gets itself updated. I know it can be done using Protocols, but I just want a simple way. Like in Android there is onResume method to perform updates whenever Activity comes into active state.
I think there is no need to share code as starting other viewController from one viewcontroller is pretty simple. I just wanted to know the better approach. Thanks in advance
UPDATE: THIS IS HOW I AM CALLING NEXT CONTROLLER OVER BASE CONTROLLER
let dialogRegisterForEventVC = UIStoryboard(name: "Main",bundle: nil).instantiateViewController(withIdentifier: "idDialogRegisterForEventVC") as! DialogRegisterForEventVC
dialogRegisterForEventVC.modalPresentationStyle = .overCurrentContext
dialogRegisterForEventVC.modalTransitionStyle = .crossDissolve
dialogRegisterForEventVC.isUserLogin = isLogin
self.present(dialogRegisterForEventVC, animated: true) {
}
You can always go with viewWillAppear Method
override func viewWillAppear(_ animated: Bool) {
print("This is called when coming back to Base View Controller")
}
Use your refresh code here. This is called when you pop your viewController
There different cases and you need to handle them for example
If you presenting another viewController on top of this you should use
override func viewWillAppear(_ animated: Bool)
If you need to refresh the view if the app comes in foreground state you need such code :
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func willResignActive(_ notification: Notification) {
refreshMyView()
}
Also, you can create delegate that your viewController can call to update this view controller. And one more option is to make your view controller observer of custom notification in Notification center and to push this notification once when you need to refresh the view controller.
If you using the notification center just be sure that your UI changes are on the main thread.
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
I have a ViewController, this view container has a class which creates 2 container views, and adds a table to the first container and a HashtagPicker for the second.
The hashTagPicker has a function which is called whenever a change to the selected hashTags happens.
question: i want to call a update table function whenever a tag is changed. How can i call a function from the hashtagclass which is defined in the class that contains the containers?
I personally like the delegate approach over notifications - the latter solution almost always leads to confusing architecture. Sadly, the example for the delegate approach, which is also the accepted answer, is even worse - it basically opens an opportunity for memory leaks. I'll explain. In the accepted solution, ParentView is holding a strong reference to HashtagPicker and, in turn, HastagPicker is holding a strong reference to ParentView, this creates a retain cycle and means neither of the controllers will be picked up by ARC and be deinitialized. So, if you are, for example, presenting ParentView from some other view and you keep going to ParentView and back, you will keep spawning new instances of ParentView (and HashtagPicker) with old ones still occupying memory.
Now, how this should have been done. I'll use exactly the same names as in the accepted answer.
The protocol should be defined like so:
// note the ": class" part
protocol HashTagPickerDelegate: class {
func picked(hashtag: String)
}
If we specify class, it means the protocol can only be used on classes. This will allow use to create weak reference, which otherwise would have been impossible.
class HashtagPicker: UIViewController {
// if HashTagPickerDelegate wouldn't be limited to class,
// we couldn't have made a weak reference here!
weak var delegate: HashTagPickerDelegate?
// at some point, you call the delegate, it can be anywhere, this is just an example
#IBAction func tappedHashtag(_ sender: Any) {
delegate?.picked(hashtag: "bla")
}
}
Now we have a weak reference to delegate, so there is not retain cycle and ARC can clean up everything nicely!
I'll throw in the rest of the code to have this as a complete answer:
class ParentView: UIViewController {
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// we are presenting the nested controller
if segue.identifier == "SegueHastagPickerContainer",
let destinationController = segue.destination as? HashtagPicker {
destinationController.delegate = self
}
}
}
extension ParentView: HashTagPickerDelegate {
func picked(hashtag: String) {
// we just got info from the child controller, do something with it!
}
}
You can use delegates as mentioned in above answer. Or you can use notifications. So here is a solution using notifications.
First of all register a notification in your parent viewController's viewDidLoad like this
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ParentViewController.someActionToBePerformed), name: "myNotification", object: nil)
Create a function in your parent viewController named same as above so it will be like
func someActionToBePerformed () {
// this will be called when hashTag is changed
// do something when hashTag is changed
}
Now you can simply post notification from your Hashtag viewController. When you want like this.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "myNotification"), object: nil)
You can use this (no notification, no delegate)
func exitButtonTapped() {
if let pdfVC : YourParnetViewController = self.parent as? YourParnetViewController {
pdfVC.removeBlurEffect()
self.removeFromParentViewController()
self.view.removeFromSuperview()
}
}
I have several controllers in my app. When my app call one function in one controller, I want to update other controllers' UI. How can I achieve that?
class FirstViewController: UIViewController {
func updateUI {...}
}
class SecondViewController: UIViewController {
func updateUI {...}
}
class ThirdViewController: UIViewController{
func updateAllUI {...} # I want call FirstViewController().updateUI() and SecondViewController().updateUI() here
}
But FirstViewController() means I create a new FirstViewController which is I don't want, and FirstViewController has already been created. So how can I call all other controllers' updateUI() in updateAllUI()
Please help, Thank you!
It's usually a pretty bad practice to have view controllers communicate directly. I would use a NSNotification to communicate between view controllers. It's convention to have the name of your notification start with a capital letter and end with the word "Notification".
class FirstViewController: UIViewController {
func updateUI {...}
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateUI", name:"TimeToUpdateTheUINotificaiton", object: nil)
}
override deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
class SecondViewController: UIViewController {
func updateUI {...}
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateUI", name:"TimeToUpdateTheUINotificaiton", object: nil)
}
override deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
class ThirdViewController: UIViewController{
func updateAllUI {
NSNotificationCenter.defaultCenter().postNotificationName("TimeToUpdateTheUINotificaiton", object: nil)
}
}
Eliminate the parentheses. You don't use them when calling a class function.
FirstViewController.updateUI()
That said.. what you're trying to do is very weird to say the least. You shouldn't use class functions to modify properties of instances of a class. If you have both View controllers on screen at the same time, you should be using a parent controller to command both of them to update their UI when you need to.
If they're not both on screen at the same time, you don't really need to update both UIs.
If you want all your view controllers to react[in this case - updating ui] to an action in one of your view controller, you try posting a Notification.. That is how you broadcast a message in iOS
You post notification from the view controller when your desired action is complete.
All other View controllers would subscribe to that notification and react to it by updating their UI when it is posted.
The below post quickly shows an example to post/observe notifications..
https://stackoverflow.com/a/2677015/4236572
http://www.idev101.com/code/Cocoa/Notifications.html might also be helpful