UIViewController & UIView: respond to screen changes from outside the object? - ios

What I'm trying to do
I'm trying to detect:
when a new UIViewController has entered the screen
when a UIViewController has left the screen
when a UIView has been added to the screen
when a UIView has left the screen
Key Point: I'm trying to detect all these changes from the outside.
Meaning: I don't want to have to respond to these changes from existing funcs inside the classes themselves, I want to be able to observe them from an outside class and react accordingly.
Bonus points: If we don't have to know ANY info about ANY of the UIViews or UIViewControllers beforehand, and we don't have to add ANY code to the views themselves, that would be amazing.
My initial thoughts is these involve KVO and listening to the UIViewController's view's window property and similar things.

You could make a BaseViewController class that inherits from UIViewController and overrides the viewDidAppear(_:) and viewDidDisappear(_:) methods. Within each of those you could post a notification.
Then make all UIViewControllers that you care about inherit from BaseViewController.
You still have to add code to all your ViewControllers, but it would just be an inheritance from a single class.
class BaseViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// viewDidAppear notification
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// viewDidDisappear notification
}
}
Same idea with a BaseView class.
class BaseView: UIView {
override func addSubview(_ view: UIView) {
super.addSubview(view)
// notification
}
override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
// notification
}
}

Related

How to get notified when a presented view controller is dismissed with a gesture?

It is possible in some cases (iPhone X, iOS 13) to dismiss presented view controllers with a gesture, by pulling from the top.
In that case, I can't seem to find a way to notify the presenting view controller. Did I miss something?
The only I found would be to add a delegate method to the viewDidDisappear of the presented view controller.
Something like:
class Presenting: UIViewController, PresentedDelegate {
func someAction() {
let presented = Presented()
presented.delegate = self
present(presented, animated: true, completion: nil)
}
func presentedDidDismiss(_ presented: Presented) {
// Presented was dismissed
}
}
protocol PresentedDelegate: AnyObject {
func presentedDidDismiss(_ presented: Presented)
}
class Presented: UIViewController {
weak var delegate: PresentedDelegate?
override func viewDidDisappear(animated: Bool) {
...
delegate?.presentedDidDismiss(self)
}
}
It is also possible to manage this via notifications, using a vc subclass but it is still not satisfactory.
extension Notification.Name {
static let viewControllerDidDisappear = Notification.Name("UIViewController.viewControllerDidDisappear")
}
open class NotifyingViewController: UIViewController {
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: .viewControllerDidDisappear, object: self)
}
}
There must be a better way to do this?
From iOS 13 Apple has introduced a new way for the users to dismiss the presented view controller by pulling it down from the top. This event can be captured by implementing the UIAdaptivePresentationControllerDelegate to the UIViewController you're presenting on, in this case, the Presenting controller. And then you can get notified about this event in the method presentationControllerDidDismiss. Here is the code example :-
class Presenting: UIViewController, UIAdaptivePresentationControllerDelegate {
func someAction() {
let presented = Presented()
presented.presentationController?.delegate = self
present(presented, animated: true, completion: nil)
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Note:
This method only gets triggered for dismissing by swiping from the top and not for the programmatic dismiss(animated:,completion:) method.
You don't need any custom delegate or Notification observer for getting the event where the user dismisses the controller by swiping down, so you can remove them.
Adopt UIAdaptivePresentationControllerDelegate and implement presentationControllerDidAttemptToDismiss (iOS 13+)
extension Presenting : UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentingViewController.presentedDidDismiss(self)
}
}
UIPresentationController has a property presentingViewController. The name is self-explanatory. You don't need the explicit delegate protocol.
The method is actually called to be able to show a dialog for example to save changes before dismissing the controller. You can also implement presentationControllerDidDismiss()
And do not post notifications to controllers which are related to each other. That's bad practice.

ViewController is blank after navigating away and returning

I'm building an app where two view controllers share a UIView subclass as the main source of UI. It works perfectly when the app is starting, but if I navigate away from the initial view, and return to it, all of the UI is lost. What do I need to do to preserve the views UI post-navigation?
My app flow is: MainView -> TableView -> DetailView
Just going from Main to Table to Main itself makes the UI vanish.
(rank isn't 10 yet, so here's a link to view: https://gfycat.com/enormousanchoredindochinesetiger)
What I do is load the UI in the UIView class through layoutSubviews, and in the UIViewControllers I set the instantiate the class, UI in the loadViews method by saying view = viewClass. I've tried adding this (view = viewClass) to viewWillAppear() as well, but it does nothing.
I've also tried creating two unique view classes in case instantiating was a problem. It didn't change anything.
ViewController:
override func loadView() {
super.loadView()
view = baseView
view.backgroundColor = .white
self.navigationController?.isNavigationBarHidden = true
requestLaunchData()
setButtonTargets()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
view = baseView
}
//How I push to the next view
#objc func upcomingButtonTapped() {
let vc = TableViewController()
navigationController?.pushViewController(vc, animated: true)
vc.upcomingLaunches = upcomingLaunches
}
UIView:
class BaseView: UIView {
//Lots of labels and buttons instantiated
override func layoutSubviews() {
super.layoutSubviews()
setUI() //adding subviews
}
//Layout configurations
}
Before it was this structure, I had all the UI (labels, buttons, a map) directly created and configured in each ViewController, which made both massive. But, it also worked.
I solved it after a night's rest.
So here's how you need to use a custom UIView class as your ViewController's view:
class YourView: UIView {
//Create your properties, views, etc.
let exampleView = UIView()
override layoutSubviews(){
super.layoutSubviews()
addSubview(exampleView)
//Add layouts, etc.
}
And then in your ViewController, in either viewDidLoad, or loadViews (like me here):
let customView = YourView()
override func loadView() {
super.loadView()
view = customView //Sets the entire view to all the UI you created in the custom class
}
The FATAL mistake I made was this:
override layoutSubviews(){
super.layoutSubviews()
if let sView = superview { //This gives you frame size and such
sView.addSubview(exampleView)
}
}
This sets the UI's memory to the SuperView, which gets lost the moment you leave that view, because of view = customView. So my controller was rendering view = customView, which was empty, because all the UI was set to the superView which was superseded by customView.
I hope that helps anyone trying to use this same architecture in the future.

Get a notification with viewWillAppear - in another VC?

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

How do I insert code in ViewDidAppear and ViewDidDisappear From other class

I want to change button image whenever UISideMenuNavigationController Appear Or Disappear.
This is the class that has a button.
class MenuViewController: UIViewController {
#IBOutlet var btnMenu: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
}
}
This is the other class that i want to insert code.
open class UISideMenuNavigationController: UINavigationController {
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// insert some code here but from MenuViewController class
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// insert some code here but from MenuViewController class
}
}
I don't want to change UISideMenuNavigationController class because it is framework from pods.
I'm using framework side menu from https://github.com/jonkykong/SideMenu
I need to change button image whenever Side Menu Appear Or Disappear. I can't find the way from ReadMe Side Menu. That's why I think need to insert the code in ViewDidAppear and ViewDidDisappear Method from Side Menu Class but don't want to break the class.
You simply need to subclass UISideMenuNavigationController and override the viewDidAppear & viewDidDisappear methods to invoke a delegate.
protocol MyUISideMenuDelegate {
func menuDidAppear(_ menu:MyUISideMenuNavigationController) -> Void
func menuDidDisappear(_ menu:MyUISideMenuNavigationController) -> Void
}
open class MyUISideMenuNavigationController: UISideMenuNavigationController {
var menuDelegate: MyUISideMenuDelegate?
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.menuDelegate?.menuDidAppear(self)
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.menuDelegate?.menuDidDisappear(self)
}
}
Then have you view controller with the button implement the protocol and set itself as the delegate.
You could also have your menu subclass send NSNotification and have any other objects that are interested subscribe to those. This way you completely decouple the menu and the other classes.
Your MenuViewController class could have a function that changes the image on the button. Eg, func changeButtonMenuImage().
Your 'UISideMenuNavigationController' (or a subclass of it) could have some sort of connection to the MenuViewController either as a property, instance variable or an IBOutlet. Eg, #IBOutlet var menuController: MenuViewController
Then your viewDidAppear and viewDidDisappear can call it's function. Eg, menuController.changeButtonMenuImage()

Swift: Default Storyboard background Image for all ViewControllers

Is there a way to set (preferably in storyboard IB) an image, which, will serve as the background image for all ViewControllers in the whole storyboard. I don't want to have to add a background image in every ViewController or replicate that in code.
I would could create a UIViewController subclass (e.g. name it DefaultViewController) that sets a specific background color in one of the initialization methods (e.g. viewDidLoad, but don't forget if you override this in a subclass of this class to call it's super method).
Then, let all your view controllers inherit from DefaultViewController
Example code:
class DefaultViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = = UIColor.redColor()
}
}
class SomeViewController: DefaultViewController {
override func viewDidLoad() {
// this call makes the background red, you can also not override this method and it will work too
super.viewDidLoad()
}
}

Resources