How to pass data to another controller on dismiss ViewController? - ios

I want to pass my data from one ViewController to another VC on VC dismiss. Is it possible?
I've tried the next method and no success:
On button click:
self.dismiss(animated: true) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "EditViewController") as! EditViewController
controller.segueArray = [values]
}
when EditViewController appears again, my segueArray is nil there.
How can I pass my data from my ViewController to the EditViewController on dismiss?

The best way to pass data back to the previous view controller is through delegates... when going from ViewController A to B, pass view controller A as a delegate and on the viewWillDisappear method for ViewController B, call the delegate method in ViewController A.. Protocols would help define the delegate and the required methods to be implemented by previous VC. Here's a quick example:
Protocol for passing data:
protocol isAbleToReceiveData {
func pass(data: String) //data: string is an example parameter
}
Viewcontroller A:
class viewControllerA: UIViewController, isAbleToReceiveData {
func pass(data: String) { //conforms to protocol
// implement your own implementation
}
prepare(for: Segue) {
/** code for passing data **/
let vc2 = ViewCOntrollerB() /
vc2.delegate = self //sets the delegate in the new viewcontroller
//before displaying
present(vc2)
}
}
Dismissing viewcontroller:
class viewControllerB: UIViewController {
var delegate: isAbleToReceiveData
viewWillDisappear {
delegate.pass(data: "someData") //call the func in the previous vc
}
}

In the dismiss completion block, you create a new instance of the EditViewController. I assume that another EditViewController instance exists back in the navigation stack, you need to find that instance & set the segueArray to values.
That you can achieve by iterating through your navigation stack's viewcontrollers like:
viewController.navigationController?.viewControllers.forEach({ (vc) in
if let editVC = vc as? EditViewController {
editVC.segueArray = ....
}
})
But I would recommend to use the delegate pattern, like:
protocol EditViewControllerDelegate: class {
func setSegueArray(segues: [UIStoryboardSegue])
}
In the viewcontroller (call it just ViewController) where the dismiss block is, declare a delegate property:
class ViewController: UIViewController {
weak var delegate: EditViewControllerDelegate?
....
}
Then on presenting the instance of (I assume from EditViewController) ViewController set the delegate like:
...
if let vc = presentingViewController as? ViewController {
vc.delegate = self
}
And conform the EditViewController to the delegate protocol like:
extension EditViewController: EditViewControllerDelegate {
func setSegueArray(segues: [UIStoryboardSegue]) {
// Do the data setting here eg. self.segues = segues
}
}

To detect when the back button is pressed on a view controller, I just use:
override func didMove(toParentViewController parent: UIViewController?) {
guard parent == nil else { return } // Back button pressed
... // Pass on the info as shown in you example
} // didMoveToParentViewController

A generic solution: (🔸 Swift 5.1 )
/**
* Returns a ViewController of a class Kind
* ## Examples:
* UIView.vc(vcKind: CustomViewController.self) // ref to an instance of CustomViewController
*/
public static func vc<T: UIViewController>(vcKind: T.Type? = nil) -> T? {
guard let appDelegate = UIApplication.shared.delegate, let window = appDelegate.window else { return nil }
if let vc = window?.rootViewController as? T {
return vc
} else if let vc = window?.rootViewController?.presentedViewController as? T {
return vc
} else if let vc = window?.rootViewController?.children {
return vc.lazy.compactMap { $0 as? T }.first
}
return nil
}

Related

How to pass a value from second view controller to first view controller through coordinator pattern

I want to use Soroush Khanlou coordinator pattern (http://khanlou.com) in my iOS app. My problem is how can I pass data back to my first view controller through coordinators?
Before using the coordinator pattern, I used a delegate(protocol) to pass data back from second view controller to the first one, because my first view controller was responsible for creating and presenting the second view controller, that was not a good solution. So I decided to use coordinator
to remove the job of app navigation from my view controllers.
This is the scenario:
class FirstViewController: UIViewController {
weak var coordinator: MainCoordinator?
func showSecondViewController(data: Data) {
coordinator?.presentSecondViewController(data: data) {[weak self] in
guard let self = self else { return }
//Do something
}
}
class MainCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start(completion: (() -> Void)?) {
navigationController.delegate = self
let firstViewController = FirstViewController.instantiate()
firstViewController.coordinator = self
navigationController.pushViewController(firstViewController, animated: false)
}
func presentSecondViewController(data: Data, completion: #escaping () -> Void) {
let child = SecondCoordinator(data: data, navigationController: navigationController)
childCoordinators.append(child)
child.parentCoordinator = self
child.start() {
completion()
}
}
func refresh(data: SomeDataType) {
//data from second view controller is here but I don't know how to pass it to my first view controller
//there is no reference to my first view controller
//what is the best way to pass data back?
}
}
class SecondCoordinator: Coordinator {
weak var parentCoordinator: MainCoordinator?
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
var data: Data!
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
convenience init(data: Data, navigationController: UINavigationController) {
self.init(navigationController: navigationController)
self.data = data
}
func start(completion: (() -> Void)?) {
let vc = SecondViewController(data: data)
vc.coordinator = self
vc.delegate = self. //here I used delegate to get data from second view controller to pass it to first view controller
navigationController.present(vc, animated: true) {
completion!()
}
}
}
extension SecondCoordinator: SecondViewControllerDelegate {
func refresh(data: SomeDataType) {
parentCoordinator?.refresh(data: data) // I need to pass this data to my first view controller
}
}
Using delegate pattern or closure callback pattern, you can pass a value from second view controller to first view controller.
If you use delegate pattern, data flow is below.
SecondViewController -> SecondViewControllerDelegate -> SecondCoordinator -> SecondCoordinatorDelegate -> FirstCoorfinator -> FirstViewController
If you use closure callback pattern, you will pass a closure as a parameter of
start method or initializer of SecondCoordinator.

Dismiss View Controller Doesn't Working in Helper Class Swift

So I have a helper class as written below:
class Helper {
static func handleTokenInvalid() {
DispatchQueue.main.async {
UIViewController().dismiss()
}
}
}
extension UIViewController {
func dismiss() {
let root = UIApplication.shared.keyWindow?.rootViewController
root?.dismiss(animated: true, completion: nil) }
}
I want to dismiss all the view controller that open and back to root of the apps. However it doesn't work. If I do the same in ordinary view controller is works. Anyone know the solution? Thank you!
Edit:
I already tried this too, but it said that found nil when wrapping optional value.
func dismiss() {
self.view.window!.rootViewController?.dismiss(animated: true, completion: nil)
}
All you are doing by this
UIViewController().dismiss
Is creating a new view controller and dismissing it.
You have to call dismiss on the actually presented View controller instance.
After two days finding the right way to dismiss my controller and couldn't find any because I think Xcode find that my current controller is nil. Instead, I use this:
let viewController = UIStoryboard(name: "DashboardPreLogin", bundle: Bundle.main).instantiateViewController(withIdentifier: "TabBarPreLoginViewController")
let appDel: AppDelegate = UIApplication.shared.delegate as! AppDelegate
appDel.window?.rootViewController = nil
appDel.window?.rootViewController = viewController
UIView.transition(with: appDel.window!, duration: 0.5, options: UIViewAnimationOptions.transitionCrossDissolve, animations: {() -> Void in appDel.window?.rootViewController = viewController}, completion: nil)
This will dismissing view controller and replace with new controller.
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
You can you use this anywhere on your Helper class
if let topController = UIApplication.topViewController() {
topController.dismiss(animated: true, completion: nil)
}
You can change the rootViewController,
UIApplication.shared.keyWindow?.rootViewController = yourController
I think there is better solution without dismissing root view controller of current application's window.
Add completion handler to your method and when you call it from inside of your controller, in closure declare that after completion will be called, you need to dismiss self (if you're pushing your controller via UINavigationController, just pop it)
static func handleTokenInvalid(completion: #escaping () -> Void) {
DispatchQueue.main.async {
completion()
}
}
Then in controller call your method with completion handler
class ViewController: UIViewController {
func call() {
Helper.handleTokenInvalid { // this is called when you call `completion` from `handleTokenInvalid`
self.dismiss(animated: true)
//self.navigationController?.popViewController(animated: true)
}
}
}

How can we pass the closures to any ViewController in the app flow?

In View Controller A,
var completionBlock: (((String) -> ()))? = nil
& I am calling the completion block like(ViewController A):
if let block = completionBlock {
block("block data to pass")
}
I don't want to pass the completion data to ViewController B, instead i want to pass to ViewController C which is presenting from ViewController B.
In simple words, i want to pass the closure data to from ViewController A to ViewController C.I know how to pass data with delegates, just curious with closures?
How can we achieve that?
If this block is something that you need to pass between several viewControllers, you have few options:
1- Pass closure as a variable: Create a variable on each new ViewController in the middle of VC-A, VC-C and pass them in between
for example:
//View Controller B:
var block:(((String) -> ()))? = nil
//Pass from A-B
if let viewcontrollerB = XXXX { //instantiate ViewController B from A
viewcontrollerB.block = self.block
}
//ViewController C:
var block:(((String) -> ()))? = nil
//Pass from B-C
if let viewcontrollerC = XXXX { //instantiate ViewController C from B
viewcontrollerC.block = self.block
}
//Call the block from ViewController C
if let block = self.block {
block("block data to pass")
}
2-Pass via Notification Center
You can pass this block from Any View Controller to Any Other:
//send notification:
let notification = Notification(name: Notification.Name("pass block"), object: block, userInfo: nil)
NotificationCenter.default.post(notification)
3-Access from shared object
Use a singleton design and create a static shared object and read/write to the object from different view controllers
//AppDelegate:
static var block:(((String) -> ()))? = nil
//ViewController A:
AppDelegate.block = XXX
//ViewController C:
if let block = AppDelegate.block {
block("block data to pass")
}
This is just a sample code i quickly wrote for you, you can modify objects based on your need. Hopefully will address your problem.
import UIKit
class ViewControllerA: UIViewController {
var block:(((String) -> ()))? = { input in
print(input)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "A"
let VCB = ViewControllerB()
let VCC = ViewControllerC()
VCC.block = block
VCB.VCC = VCC
self.navigationController?.pushViewController(VCB, animated: true)
}
}
class ViewControllerB: UIViewController {
var VCC:ViewControllerC?
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .gray
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let VCC = VCC {
self.present(VCC, animated: true, completion: nil)
}
}
}
class ViewControllerC: UIViewController {
var block:(((String) -> ()))? = nil
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .yellow
//Will run the block that has been passed
block?("test")
}
}

ViewController Pushing Swift From One VC to Another VC And Returning back

Consider two view controller Controller1 and Controller2, I have created a form of many UITextField in controller 1, in that when a user clicks a particular UITextField it moves to Controller2 and he selects the data there.
After selecting the data in Controller2 it automatically moves to Controller1, while returning from controller2 to controller1 other UITextfield data got cleared and only the selected data from controller2 is found. I need all the data to be found in the UITextfield after selecting.
Here is the code for returning from Controller2 to Controller1
if(Constants.SelectedComplexName != nil)
{
let storyBoard: UIStoryboard = UIStoryboard(name: "NewUserLogin", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "NewUser") as! NewUserRegistrationViewController
self.present(newViewController, animated: true, completion: nil)
}
To pass messages you need to implement Delegate.
protocol SecondViewControllerDelegate: NSObjectProtocol {
func didUpdateData(controller: SecondViewController, data: YourDataModel)
}
//This is your Data Model and suppose it contain 'name', 'email', 'phoneNumber'
class YourDataModel: NSObject {
var name: String? //
var phoneNumber: String?
var email: String?
}
class FirstViewController: UIViewController, SecondViewControllerDelegate {
var data: YourDataModel?
var nameTextField: UITextField?
var phoneNumberTextField: UITextField?
var emailTextField: UITextField?
override func viewDidLoad() {
super.viewDidLoad()
callWebApi()
}
func callWebApi() {
//After Success Fully Getting Data From Api
//Set this data to your global object and then call setDataToTextField()
//self.data = apiResponseData
self.setDataToTextField()
}
func setDataToTextField() {
self.nameTextField?.text = data?.name
self.phoneNumberTextField?.text = data?.phoneNumber
self.emailTextField?.text = data?.email
}
func openNextScreen() {
let vc2 = SecondViewController()//Or initialize it from storyboard.instantiate method
vc2.delegate = self//tell second vc to call didUpdateData of this class.
self.navigationController?.pushViewController(vc2, animated: true)
}
//This didUpdateData method will call automatically from second view controller when the data is change
func didUpdateData(controller: SecondViewController, data: YourDataModel) {
}
}
class SecondViewController: UIViewController {
var delegate: SecondViewControllerDelegate?
func setThisData(d: YourDataModel) {
self.navigationController?.popViewController(animated: true)
//Right After Going Back tell your previous screen that data is updated.
//To do this you need to call didUpdate method from the delegate object.
if let del = self.delegate {
del.didUpdateData(controller: self, data: d)
}
}
}
push your view controller instead of a present like this
if(Constants.SelectedComplexName != nil)
{
let storyBoard: UIStoryboard = UIStoryboard(name: "NewUserLogin", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "NewUser") as! NewUserRegistrationViewController
self.navigationController?.pushViewController(newViewController, animated: true)
}
and then pop after selecting your data from vc2 like this
self.navigationController?.popViewController(animated: true)
and if you are not using navigation controller then you can simply call Dismiss method
self.dismiss(animated: true) {
print("updaae your data")
}
There are a few ways to do it, but it usually depends on how you move from VC#1 to VC#2 and back.
(1) The code you posted implies you have a Storyboard with both view controllers. In this case create a segue from VC#1 to VC#2 and an "unwind" segue back. Both are fairly easy to do. The link provided in the comments does a good job of showing you, but, depending on (1) how much data you wish to pass back to VC#1 and (2) if you wish to execute a function on VC#2, you could also do this:
VC#1:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowVC2" {
if let vc = segue.destination as? VC2ViewController {
vc.VC1 = self
}
}
}
VC#2:
weak var VC1:VC1ViewController!
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParentViewController {
VC1.executeSomeFunction()
}
}
Basically you are passing the entire instance of VC1 and therefore have access to everything that isn't marked private.
(2) If you are presenting/dismissing VC#2 from VC#1, use the delegate style as described by one of the answers.
VC#1:
var VC2 = VC2ViewController()
extension VC1ViewController: VC2ControlllerDelegate {
func showVC2() {
VC2.delegate = self
VC2.someData = someData
present(VC2, animated: true, completion: nil)
}
function somethingChanged(sender: VC2ViewController) {
// you'll find your data in sender.someData, do what you need
}
}
VC#2:
protocol VC2Delegate {
func somethingChanged(sender: VC2ViewController) {
delegate.somethingChanged(sender: self)
}
}
class DefineViewController: UIViewController {
var delegate:DefineVCDelegate! = nil
var someData:Any!
func dismissMe() {
delegate.somethingChanged(sender: self)
dismiss(animated: true, completion: nil)
}
}
}
Basically, you are making VC#1 be a delegate to VC2. I prefer the declaration syntax in VC#2 for `delegate because if you forget to set VC#1 to be a delegate for VC#2, you test will force an error at runtime.

How to get the current visible viewController from AppDelegate

So, I'm using the method bellow from UIApplication extension to get the top view controller:
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
But the problem is: It always returns UIViewController. But I need to check if it is MyViewController for example. How do I achieve that?
Do conditional casting on the return value to safely check its type.
if let currentVC = UIApplication.topViewController() as? MyViewController {
//the type of currentVC is MyViewController inside the if statement, use it as you want to
}
Your whole function implementation is flawed, if it actually worked, it would lead to infinite recursion. Once you find out the type of your current top view controller in your if statements, you are calling the same function again with the current root controller as its input value. Your function only ever exists, if it reaches either a call from a view controller, whose class is none of the ones specified in your optional bindings.
Moreover, your whole implementation doesn't do anything at the moment. You find out the type of your root view controller, but than you upcast it by returning a value of type UIViewController.
You can do a conditional check with an if-let statement like this:
if let presented = controller?.presentedViewController as? MyViewController {
// it is a MyViewController
}
You can also just directly check if the UIViewController is that type of class like this:
if controller?.presentedViewController is MyViewController {
// it is a MyViewController
}
Try this:
if let presented = controller?.presentedViewController as? MyViewController {
...
You can check it in following ways
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
else if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
else if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
// Answer
if let topVC = AppDelegate.topViewController() as? MyViewController {
// Here your topVC is MyViewController
}
// or
if let topVC = AppDelegate.topViewController() {
if topVC is MyViewController {
// Here your topVC is MyViewController
}
}
To use the UIViewController as MyViewController:
if let myViewController = UIApplication.topViewController() as? MyViewController { ... }
or if you just want to check that the UIViewController is of type MyViewController:
if UIApplication.topViewController() is MyViewController { ... }

Resources