Trigger events when ViewController covered by a presented ViewController - ios

I would like to process code when a ViewController is no longer visible due to presenting a new ViewController.
I cannot use ViewWillDisappear etc since the controller is not technically ever dismissed from the stack - you just can't see it.
What process can I use so that code runs when the controller is no longer visible (i.e. topmost) and when it becomes visible again?
EDIT:
Seems some confusion here - not sure why.
I have a viewcontroller.
I use the following code to present another controller
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navController = storyboard.instantiateViewControllerWithIdentifier("NavController") as! UINavigationController
let thisController = navController.viewControllers[0] as! MyController
self.presentViewController(navController, animated: true, completion: nil)
This controller does not trigger a viewWillDisappear on the previous controller since the previous view is not removed - just hidden.
I need to process code when this view is hidden (i.e. not visible) and, more importantly, process code when it becomes visible again.

When presenting a UIViewController if the presentation style has been set to UIModalPresentationOverCurrentContext it doesn't call the viewWillDisappear and related methods as the view never disappears or gets hidden.
A simple test to check if thats the case would be to set the NavController that you are using to have a clear background color. If you do this and present the NavController and you can still view the first UIViewController below your NavController content. Then you are using UIModalPresentationOverCurrentContext and that is why the viewDidDisappear isn't called.
Have a look at the answer referenced by Serghei Catraniuc (https://stackoverflow.com/a/30787112/4539192).

EDIT: This is in Swift 3, you can adjust your method accordingly if you're using an older version of Swift
If you won't be able to figure out why viewDidAppear and viewDidDisappear are not called, here's a workaround
protocol MyControllerDelegate {
func myControllerWillDismiss()
}
class MyController: UIViewController {
var delegate: MyControllerDelegate?
// your controller logic here
func dismiss() { // call this method when you want to dismiss your view controller
// inform delegate on dismiss that you're about to dismiss
delegate?.myControllerWillDismiss()
dismiss(animated: true, completion: nil)
}
}
class PresentingController: UIViewController, MyControllerDelegate {
func functionInWhichYouPresentMyController() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navController = storyboard.instantiateViewController(withIdentifier: "NavController") as! UINavigationController
let thisController = navController.viewControllers[0] as! MyController
thisController.delegate = self // assign self as delegate
present(navController, animated: true, completion: {
// place your code that you want executed when it disappears here
})
}
func myControllerWillDismiss() {
// this method will be called now when MyController will dismiss
// place your code that you want executed when it re-appears here
}
}

Firstly, thanks to Serghei for his time in helping work through this.
To clarify, both my potential presented controllers were set to Full Screen presentation style in the storyboard, however one was being set to Custom via a piece of pasted code dealing with the presentation. I can't find the error with the other.
However, if I force a presentation style of Full Screen as part of the presenting process then all is ok.
Hopefully my frustrating afternoon can help to save someone else's - always try to understand the implications and processes involved in pasted snippets.

Related

Best way to present a view controller

i'm working on multiple frameworks and my question is just a "philosophic" one.
I created an utility function to show view controllers
static func presentViewController(identifier: String, storyboardName: String = "Main", presentationStyle: UIModalPresentationStyle = .fullScreen){
let storyboard = UIStoryboard(name: storyboardName, bundle: InternalConstants.bundle)
var viewResult: UIViewController
if #available(iOS 13.0, *) {
viewResult = storyboard.instantiateViewController(identifier: identifier)
} else {
viewResult = storyboard.instantiateViewController(withIdentifier: identifier)
}
viewResult.modalPresentationStyle = presentationStyle
var top = UIApplication.shared.keyWindow?.rootViewController
while top?.presentedViewController != nil {
top = top!.presentedViewController
}
top!.present(viewResult, animated: true, completion: nil)
}
First of all, is this a correct way to present a view controller or is there a better way?
Then, is it better to present a view controller in a navigation controller or not?
First of all, is a correct way to present a view controller or there is a better way?
instead of a utility make it inside
extension UIViewController {
func ......
}
Then, is better to present a view controller in a navigation controller or not?
a nav is oriented for push/pop but it's also not wrong to use it to present another vc
First of all, is this a correct way to present a view controller or is there a better way?
as long as it's working then it's correct it's just your way of doing this specific thing, but is it the right thing to do as for an iOS and UIKit standpoint the answer is no it's usually is a bad thing to present a viewController by looking at the rootViewController's presentedViewController because it's not guaranteed that the last presentedViewController you find is a good thing to present on it and you won't know until it breaks, that presentedViewController could be a UISearchController and if you use UIContentContainer or ContainerView from storyboards, you might have a small viewController that is just a UISlider at the end, this could be bad for viewController appearance and disappearance
another problem that you will face is when you need to pass data to and from the viewController that you presenting by using this approach you don't even have a reference to the viewController you are presenting, because you are only passing an identifier
from an MVC standpoint you should never try to present viewController from a UIView by calling your function from your view directly Thats Bad Practice
if you take a look at the UIKit SDK if you ever try to present any system UIViewController you will find that you have the responsibility of instantiating and presenting the vc for example UIImagePickerController, UIActivityViewController, UIDocumentPickerViewController, UIDocumentMenuViewController, UIPrinterPickerController, UIVideoEditorController
Apple themselves didn't go for providing a function to present theirs system vcs
instead if you are developing a framework and don't want to give users access to your viewControllers you should make you own window and give it a rootViewController
Apple also has many examples for this too, in the AuthenticationServices framework for security reasons you should not have a reference to the safari web browser they have something called ASWebAuthenticationSession that controls the flow of presenting and dismissing the Safari Web ViewController by calling start() and cancel() functions
also the users of your framework will not always want to present your viewController with the default presentation animation they might want to use custom viewContollers animations which they will need access to the transitioningDelegate property on the viewController
imagine every public useful property on UIViewController will not be accessible if you go with this approach
Then, is it better to present a view controller in a navigation controller or not?
as for this part it's totally fine to present anything on a navigationController
Storyboards headaches
as for the storyboards initialization headaches there are plenty of articles out there talking about optimizing the storyboards initialization call site for that I would recommend doing something like this
extension UIStoryboard {
enum AppStoryBoards: String {
case
login,
main,
chat,
cart
}
convenience init(_ storyboard: AppStoryBoards, bundle: Bundle? = nil) {
self.init(name: storyboard.rawValue.prefix(1).capitalized + storyboard.rawValue.dropFirst(), bundle: bundle)
}
}
This way you can initialize a storyboards using enum which improves the call site to be like this
let login = UIStoryboard.init(.login)
then you can have another extension for view controller initialization like this
extension UIStoryboard {
func instantiateInitialVC<T: UIViewController>() -> T {
return self.instantiateInitialViewController() as! T
}
func instantiateVC<T: UIViewController>(_: T.Type) -> T {
return self.instantiateViewController(withIdentifier: String(describing: T.self)) as! T
}
}
and you can then call it like this
let loginVC = UIStoryboard.init(.login).instantiateInitialVC()
or this
let loginVC = UIStoryboard.init(.login).instantiateVC(LoginViewController.self)
by doing that you improve your overall code for presenting any viewController
let dvc = UIStoryboard.init(.login).instantiateVC(LoginViewController.self)
dvc.plaplapla = "whatever"
present(dvc, animated: true)

UINavigationController popToViewController not popping

Been working with UIKit for years now. It's amazing how issues like this seem to pop-up out of the blue.
I have a simple navigation setup:
UINavigationController
HomeViewController [push]
DetailViewController [push]
ModalViewController [modal]
A root navigation controller with 2 children pushed onto the stack. Then a modal presented from the root nav controller.
For some reason, the following snippet of code isn't working as expected:
extension UINavigationController {
func popToViewController(_ vc: UIViewController, animated: Bool, completion: #escaping ([UIViewController]?)->()) {
let popped = popToViewController(viewController, animated: animated)
if let coordinator = self.transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion(popped)
}
}
else {
completion(popped)
}
}
}
using the extension:
navigationController.popToViewController(
homeViewController,
animated: true
)
No errors, warnings or crashes occur. UI is still fully responsive. But the DetailViewController in the stack is never popped. Inspecting the extension's popped variable, results in an empty array - which makes sense as the DetailViewController is clearly not removed from the stack.
What could prevent a navigation controller from popping a valid vc off of it's stack?
Things I've checked:
homeViewController is in the stack already, and I'm asking it to pop to the same instance. i.e. navigationController.viewControllers.contains(homeViewController) == true
I'm on the main thread. i.e. Thread.isMainThread == true
navigationController.viewControllers returns the same array before & after calling popToViewController(_vc:animated:)
Manually figuring out what vc's need to be popped, and calling setViewControllers(_ vcs:animated:) with the vcs I want to keep (in this case, just the HomeViewController instance). This still has the same issue.
I want to say this has something to do with popping view controllers off the stack from behind a modal presentation. But, as far as I know this is an okay thing to do. Plus, I've done it before and have had no issues in the past.

How to move through Views without reload them again if I return back Swift

I'll give an example of what I want so it's not so confusing:
Example:
Let's say that I have a map that adds every time that my user scrolls 3 annotations dynamically. Now I have a button under the map and when I press it I go to another viewController do what I want and get back to the viewController with the map, now I want to find all the annotations that my map had and not reload the view at all.
I used to use this function that I made to move between viewControllers:
func move(identifier: String , viewController : UIViewController) -> Void {
let mstoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc: UIViewController = mstoryboard.instantiateViewControllerWithIdentifier(identifier)
viewController.presentViewController(vc, animated: true, completion: nil)
}
I also tried this:
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("view") as? MyViewcontroller
self.presentViewController(vc!, animated: true, completion: nil)
These two when I use them the viewcontroller that appears is calling viewDidload so its like it appeared for the first time.
Another example is the tabBarViewController if you notice when you navigate through tabs nothing reloads (only function that is called is viewDidAppear )
EDIT
test file
The problem is caused by the fact that the map controller gets deallocated when navigating back to the other controller, and another one is created when you want to move again to the map screen.
What you need is to hold on onto the same controller instance, and present that one. Keeping a strong reference in the presenting controller would suffice.
class PresentingController {
// making the property lazy will result in the getter code
// being executed only when asked the first time
lazy var mapController = { () -> UIViewController in
let mstoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
return mstoryboard.instantiateViewControllerWithIdentifier("mapControllerIdentifier")
}()
func moveToMap() {
// simply use the mapController property
// the property reference will make sure the controller won't
// get deallocated, so every time you navigate to that screen
// you'll get the same controller
presentViewController(mapController, animated: true, completion: nil)
}
}
According to the same project you posted, you instantiate a new UIViewController when going from view 2 back to view 1 and that is why your viewDidLoad gets called again and your entire map view is reloaded.
In your sample project, instead of
lazy var mapController2 = { () -> UIViewController in
let mstoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
return mstoryboard.instantiateViewController(withIdentifier: "first")
}
You should just dismiss your view 2 on the button press.
#IBAction func butto(_ sender: AnyObject) {
//Your initial code
//PresentingController().moveToMap(self, flag: 1)
self.dismiss(animated: true, completion: nil)
}
When you present a new UIViewController, the older UIViewController is not removed from memory, it is just hidden behind the new UIViewController. So whenever you wish to go back to a UIViewController with the previous state maintained, all you need to do is close the new UIViewController
However, if you are doing some tasks that you performed on your second UIViewController that you wish to be reflected in your initial UIViewController, you will have to setup closures to update your initial UIViewController.

Using viewDidAppear to present a View Controller, re-opening it when it's closed

In my App, I've created a new storyboard that serves as a very basic tutorial for how to use certain features. (Instructions.storyboard). This storyboard has it's own class - InstructionsVC.swift
I want to present InstructionsVC when MainVC loads within viewDidAppear.
It works great. Fires up on App load just like it's supposed to. The problem occurs when I press the [Close] button on the Instructions interface. It closes the VC, fades to the main screen, and then immediately fires the Instructions VC back up.
How can I prevent the Instructions VC from loading back up once it's closed?
func openInstructions() {
let storyboard = UIStoryboard(name: "Instructions", bundle: nil)
let instructionsView = storyboard.instantiateViewController(withIdentifier: "instructionsStoryboardID")
instructionsView.modalPresentationStyle = .fullScreen
instructionsView.modalTransitionStyle = .crossDissolve
self.present(instructionsView, animated: true, completion:nil)
}
override func viewDidAppear(_ animated: Bool) {
openInstructions()
}
And within my instructions class, I have the following action on the close button:
#IBAction func closeButtonPressed(_ sender: UIButton) {
let presentingViewController: UIViewController! = self.presentingViewController
presentingViewController.dismiss(animated: true, completion: nil)
}
Note - I'd rather not use UserDefaults to resolve this, because I'm going to be incorporating something similar in other parts of the App and don't want to resort to UserDefaults to achieve the desirable behavior.
Thanks in advance buddies!
viewWillAppear and viewDidAppear are called every time a view controller's content view becomes visible. That includes the first time it's rendered and when it's shown again after being covered by a modal or by another view controller being pushed on top of it in a navigation stack.
viewDidLoad is only called once when a view controller's content view has been loaded, but before it is displayed. Thus when viewDidLoad is called it may be too soon to invoke your second view controller.
You might want to add an instance variable hasBeenDisplayed to your view controller. In viewDidAppear, check hasBeenDisplayed. If it's false, display your second view controller and set hasBeenDisplayed to true.

Removing a view controller from memory when instantiating a new view controller

In my app, I am instantiating new view controllers instead of using segues because it looks better in animations as a result, my views keep running in the background. This causes large memory leaks.
My code to go back to the main screen is:
let mainStoryboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("MainScreen") as UIViewController
self.presentViewController(vc, animated: false, completion: nil)
This view controller is still active in the background and therefore shouldn't be instantiated again. How do I do this.
When I close my view controller using the above code, it also does not unload it, it keeps running in the background. How do I make it unload as soon as the screen disappears.
I have tried doing
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
view.removeFromSuperview()
view = nil
}
However this does not work properly. How do I properly destroy a view controller from memory when exiting a view controller in this manner.
You need only to use:
EDIT Swift 4.2
self.dismiss(animated:true, completion: nil)
The rest of work is doing by ARC
To help you during your debug you can add also this code:
if let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window {
if let viewControllers = window.rootViewController?.children {
for viewController in viewControllers {
print(viewController.debugDescription)
}
}
}
An important reason for this problem is related to the memory management!
if you have 'strong reference' or 'delegate' or 'closure' or other things like this, and you didn't managed these objects, your view controller has strong reference and never be closed.
you should get 'deinit' callback in view controller after than viewDidDisappear called. if 'deinit' not called so your view controller still is alive and it has strong reference.

Resources