How to present a ViewController modally from the presentingViewController? - ios

I have 3 view controllers.
let's name them as A, B & C.
A presents B and then C should be present from A after dismissing B.
A <=> B
A -> C
How can I achieve this?
If the question is unclear then do let me know, I would be happy to edit it.

Well, I achieved it this way.
Note: I am inside B.
let cViewController = // getting a handle of this view controller from Storyboard
let aViewController = self.navigationController?.presentingViewController
self.dismiss(animated: true) {
aViewController?.present(cViewController, animated: true)
}

You can use custom notification observer like below:
In Controller A:
override func viewDidLoad() {
super.viewDidLoad()
// Register to custom notification
NotificationCenter.default.addObserver(self, selector: #selector(presentC), name: NSNotification.Name(rawValue: "BDismissed"), object: nil)
// Rest of your code
}
func presentC {
// Controller C presentation code goes here
}
In Controller B:
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "BDismissed"), object: nil, userInfo: nil)
}

Inside B try this
self.dismiss(animated: true) {
let aVC = UIApplication.shared.keyWindow?.rootViewController as! AVC
let cVC = ///
aVC.present(cVC, animated: true, completion: nil)
}

Write a protocol in B like:
protocol VCBDelegate {
func VCBDismissed()
}
Class VCB: UIViewController {
weak var delegate: VCBDelegate?
....
}
Now where you are dismissing B, call the delegate method in completion.
func dismissB() {
self.dismiss(animated: true) {
self.delegate.VCBDismissed()
}
}
Now conform this protocol in A.
extension VCA: VCBDelegate {
func VCBDismissed() {
//Here you present C
.....
}
}
Don't forget to make the delegate self where you are presenting B.
Hope this helps, for any queries please feel free to leave a comment.

You can use closures, it's better and simple.
Your A will present B and give it a closure to call when it dismiss, this closure will present C.
Here is an example :
class ViewControllerA : UIViewController{
func showViewControllerB(){
let vc = ViewControllerB()
vc.callOnDismiss = { [weak self] in
self?.showViewControllerC()
}
self.present(vc, animated: true, completion: nil);
}
func showViewControllerC(){
let vc = ViewControllerC()
self.present(vc, animated: true, completion: nil);
}
}
class ViewControllerB : UIViewController{
var callOnDismiss : () -> () = {}
func actionOnDismiss(){
self.dismiss(animated: true, completion: nil)
self.callOnDismiss()
}
}
class ViewControllerC : UIViewController{
}

Related

How to test if view controller is dismissed or popped

i want to write an unit test for my function, here is code:
func smartBack(animated: Bool = true) {
if isModal() {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
This method automatically chooses dismiss or pop. So, how i can check if viewcontroller popped or dismissed after this function? Thank you for help
You can check the view controller's isBeingDismissed property in either its viewWillAppear or viewDidAppear function.
See https://developer.apple.com/documentation/uikit/uiviewcontroller/2097562-isbeingdismissed.
func smartBack(animated: Bool = true) will be:
func smartBack(animated: Bool = true) {
if self.navigationController?.viewControllers.first == self {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: true)
}
}
You can use property self.isBeingPresented, will return true is view controller presented otherwise false if pushed.
You could check the viewControllers stack and see if your viewController is included or not, using:
self.navigationController.viewControllers
This will return a [UIViewController] contained in the navigationController stack.
Personally I would use Mocks to track when certain methods are called.
You can do this like so:
class MockNavigationController: UINavigationController {
var _popCalled: Bool = false
override func popViewController(animated: Bool) -> UIViewController? {
_popCalled = true
return self.viewControllers.first
}
}
Then anytime your code calls popViewController, the _popCalled value would be updated but it wouldn't actually pop anything. So you can assert the _popCalled value to make sure that the expected call happened.
This makes it easy to test that an expected thing happened and also prevents you running actual code in your tests. This method could easily be a service call, or db update, setting a flag etc, so can be much safer.
They can be difficult to understand at first though. I would suggest reading up on them before heavy use.
A full example in a playground:
import UIKit
import PlaygroundSupport
import MapKit
class ClassUnderTest: UIViewController {
var isModal: Bool = false
func smartBack(animated: Bool = true) {
if isModal {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
}
class MockNavigationController: UINavigationController {
var _popCalled: Bool = false
override func popViewController(animated: Bool) -> UIViewController? {
_popCalled = true
return self.viewControllers.first
}
}
class MockClassUnderTest: ClassUnderTest {
var _mockNavigationController = MockNavigationController()
override var navigationController: UINavigationController? {
return _mockNavigationController
}
var _dismissCalled: Bool = false
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
_dismissCalled = true
}
}
var subject = MockClassUnderTest()
subject.isModal = true
subject.smartBack();
var navigation = subject.navigationController as! MockNavigationController
print(subject._dismissCalled)
print(navigation._popCalled)
OUTPUT:
true
false
subject = MockClassUnderTest();
subject.isModal = false
subject.smartBack();
navigation = subject.navigationController as! MockNavigationController
print(subject._dismissCalled)
print(navigation._popCalled)
OUTPUT:
false
true
In this example, you are overriding the dismiss and pop methods which would be called in either case. In your unit test you would just assert the stubbed values (_popCalled) are true or false for your expectations.
I solved in this way. I have needed to test a simple method that contains this: dismiss(animated: true, completion: nil) and I made a temporal mock that simulates a viewController that do a push to my MainController which it is where I apply the dismissView.
func testValidatesTheDismissOfViewController() {
// Given
let mockViewController: UIViewController = UIViewController()
let navigationController = UINavigationController(rootViewController: mockViewController)
// Create instance of my controller that is going to dismiss.
let sut: HomeWireFrame = HomeWireFrame().instanceController()
navigationController.presentFullScreen(sut, animated: true)
// When
sut.loadViewIfNeeded()
sut.closeView()
// Create an expectation...
let expectation = XCTestExpectation(description: "Dismiss modal view: HomeViewController")
// ...then fulfill it asynchronously
DispatchQueue.main.async { expectation.fulfill() }
wait(for: [expectation], timeout: 1)
// Then - if its different of my home controller
XCTAssertTrue(!(navigationController.topViewController is HomeViewController))
}
I hope can help, I´m here to any doubt.
It is worked for me:
func smartBack(animated: Bool = true) {
if self.navigationController == nil {
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: true)
}
}

How to implement routing in swift based on protocols

I would like to implement routing in Swift like ReactJS, I have implemented protocols to serve for Routing.
But it's getting crashed in UIViewController extention. Can anyone help me with the solution.
Here is my code.
import Foundation
import UIKit
extension UIViewController {
func presented(_ animated: Bool) {
print("\(#function)")
present(Route.destination, animated: animated,
completion: nil)
}
func pushed(_ animated: Bool) {
print("\(#function)")
_ = navigationController?.pushViewController(Route.destination,
animated: true)
}
}
protocol Router {
static func toController <T: UIViewController>(_ controller:T,
params: Any) -> T
}
class Route : Router {
static var destination: UIViewController!
static func toController<T:UIViewController>(_ controller: T,
params: Any) -> T {
let viewController : T = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: String(describing: T.self)) as! T
destination = viewController
return viewController
}
}
class ViewController: UIViewController {
#IBAction func navigate() {
Route.toController(SecondViewControlller(), params: [])
.presented(true)
}
}
The app is crashing because you are going to present the same
viewController on itself.
The reason is below method takes destination viewController as an argument and return itself as destination.
static func toController <T: UIViewController>(_ controller:T, params: Any) -> T
In addition to that, whenever presented(_ animated: Bool) gets called from Route.toController(SecondViewControlller(), params: []).presented(true), self and Route.destination are same. So, it leads to presenting the same viewController on itself and causing sort of below error or crashing the application.
Attempt to present on
whose view is not in the
window hierarchy!
Try this:
extension UIViewController {
func presented(_ animated: Bool) {
print("\(#function)")
self.present(Route.destination, animated: animated, completion: nil)
}
func pushed(_ animated: Bool) {
print("\(#function)")
_ = self.navigationController?.pushViewController(Route.destination, animated: true)
}
}
protocol Router {
static func toController <T: UIViewController, T2: UIViewController>(_ controller: T2, from source: T, params: Any) -> T
}
class Route : Router {
static var destination: UIViewController!
static func toController <T: UIViewController, T2: UIViewController>(_ controller: T2, from source: T, params: Any) -> T {
let viewController : T2 = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: String(describing: T2.self)) as! T2
destination = viewController
return source
}
}
//Your ViewController.swift
#IBAction func onButtonTap(_ sender: Any) {
Route.toController(SecondViewControlller(), from: self, params: []).presented(true)
}

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.

Not possible to transfer the data back to the ViewController

I am having issues trying to pass the data back to the ViewController (from BarCodeScannerViewController to TableViewController)
SecondVC (BarCodeScannerViewController.swift):
#objc func SendDataBack(_ button:UIBarButtonItem!) {
if let presenter = self.presentingViewController as? TableViewController {
presenter.BarCode = "Test"
}
self.dismiss(animated: true, completion: nil)
}
FirstVC (TableViewController.swift):
// The result is (BarCode - )
var BarCode: String = ""
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("BarCode - \(BarCode)")
}
Each time ViewWillAppear is running the value is not set, what could be causing this issue?
You should use the delegate pattern. I doubt in your code above that self.presentingViewController is actually set.
An example of using the delegate pattern for this:
// BarCodeScannerViewController.swift
protocol BarcodeScanningDelegate {
func didScan(barcode: String)
}
class BarCodeScannerViewController: UIViewController {
delegate: BarcodeScanningDelegate?
#objc func SendDataBack(_ button:UIBarButtonItem!) {
delegate?.didScan(barcode: "Test")
}
}
// TableViewController
#IBAction func scanBarcode() {
let vc = BarCodeScannerViewController()
vc.delegate = self
self.present(vc, animated: true)
}
extension TableViewController: BarcodeScanningDelegate {
func didScan(barcode: String) {
print("[DEBUG] - Barcode scanned: \(barcode)")
}
}

How can I pop specific View Controller in Swift

I used the Objective-C code below to pop a specific ViewController.
for (UIViewController *controller in self.navigationController.viewControllers) {
if ([controller isKindOfClass:[AnOldViewController class]]) {
//Do not forget to import AnOldViewController.h
[self.navigationController popToViewController:controller
animated:YES];
break;
}
}
How can I do that in Swift?
Try following code:
for controller in self.navigationController!.viewControllers as Array {
if controller.isKind(of: ViewController.self) {
self.navigationController!.popToViewController(controller, animated: true)
break
}
}
Swift 5
To pop to the latest instance of a specific class, for example SomeViewController:
navigationController?.popToViewController(ofClass: SomeViewController.self)
But you need to add ths UINavigationController extension:
extension UINavigationController {
func popToViewController(ofClass: AnyClass, animated: Bool = true) {
if let vc = viewControllers.last(where: { $0.isKind(of: ofClass) }) {
popToViewController(vc, animated: animated)
}
}
}
For Swift 3+
let viewControllers: [UIViewController] = self.navigationController!.viewControllers
for aViewController in viewControllers {
if aViewController is YourViewController {
self.navigationController!.popToViewController(aViewController, animated: true)
}
}
From Swift 4.0 and Above
for controller in self.navigationController!.viewControllers as Array {
if controller.isKind(of: DashboardVC.self) {
_ = self.navigationController!.popToViewController(controller, animated: true)
break
}
}
This is working Perfect.
I prefer a generic way to do it.
I have this extension for the UINavigationController :
extension UINavigationController {
func backToViewController(vc: Any) {
// iterate to find the type of vc
for element in viewControllers as Array {
if "\(element.dynamicType).Type" == "\(vc.dynamicType)" {
self.popToViewController(element, animated: true)
break
}
}
}
}
Let's say I have a FOHomeVC class (who is a UIViewController) instantiated in the navigation stack.
So I would do this in my code:
self.navigationController?.backToViewController(FOHomeVC.self)
I have added an extension to UINavigationController which helps you to find if that controller exist in navigation stack. If yes then it will be popped to that controller or else you pass new controller to push with pushController param.
extension UINavigationController {
func containsViewController(ofKind kind: AnyClass) -> Bool {
return self.viewControllers.contains(where: { $0.isKind(of: kind) })
}
func popPushToVC(ofKind kind: AnyClass, pushController: UIViewController) {
if containsViewController(ofKind: kind) {
for controller in self.viewControllers {
if controller.isKind(of: kind) {
popToViewController(controller, animated: true)
break
}
}
} else {
pushViewController(pushController, animated: true)
}
}
}
Swift 4 / Swift 5
for controller in self.navigationController!.viewControllers as Array {
if controller.isKind(of: HomeViewController.self) {
self.navigationController!.popToViewController(controller, animated: true)
break
}
}
I prefer a "real generic" and more functional approach.
So I came up with following UINavigationController extension functions. You can also use the first function, for anything else, where you just need to access a specific VC in the navigation stack.
Extensions
extension UINavigationController {
func getViewController<T: UIViewController>(of type: T.Type) -> UIViewController? {
return self.viewControllers.first(where: { $0 is T })
}
func popToViewController<T: UIViewController>(of type: T.Type, animated: Bool) {
guard let viewController = self.getViewController(of: type) else { return }
self.popToViewController(viewController, animated: animated)
}
}
Usage
self.navigationController?.popToViewController(of: YourViewController.self, animated: true)
This should work at least in Swift 4 and 5.
Find your view controller from navigation stack and pop to that view controller if it exists
for vc in self.navigationController!.viewControllers {
if let myViewCont = vc as? VCName
{
self.navigationController?.popToViewController(myViewCont, animated: true)
}
}
swift5
let controllers : Array = self.navigationController!.viewControllers
self.navigationController!.popToViewController(controllers[1], animated: true)
Swift 5 Answer of #PabloR is Here :
extension UINavigationController {
func backToViewController(vc: Any) {
// iterate to find the type of vc
for element in viewControllers as Array {
if "\(type(of: element)).Type" == "\(type(of: vc))" {
self.popToViewController(element, animated: true)
break
}
}
}
}
Usage :
self.navigationController?.backToViewController(vc: TaskListViewController.self)
In latest swift
#IBAction func popToConversationsVC(_ sender: UIButton) {
if (self.navigationController != nil) {
for vc in self.navigationController!.viewControllers {
if vc is ConversationsVC {
self.navigationController?.popToViewController(vc, animated: false)
}
}
}
}
For Swift 4.0 and above Using Filter
guard let VC = self.navigationController?.viewControllers.filter({$0.isKind(of: YourViewController.self)}).first else {return}
self.navigationController?.popToViewController(VC, animated: true)
Please use this below code for Swift 3.0:
let viewControllers: [UIViewController] = self.navigationController!.viewControllers as [UIViewController];
for aViewController:UIViewController in viewControllers {
if aViewController.isKind(of: YourViewController.self) {
_ = self.navigationController?.popToViewController(aViewController, animated: true)
}
}
I needed to use this, because in some cases app crashes:
if let navVC = self.navigationController {
let views = navVC.viewControllers as Array
for controller in views {
if controller.isKind(of: YourVC.self) {
navVC.popToViewController(controller, animated: true)
return
}
}
}
This solution worked for me :)
extension UINavigationController {
func backToViewController(_ viewController: AnyClass, animated: Bool) {
guard let viewController = self.viewControllers.first(where: {$0.isKind(of: viewController)}) else { return }
self.popToViewController(viewController, animated: animated)
}
}
I adapt from all answer above. It look like Yakup Ad answer, because it's very short way.
I force type by using generic for argument, that make sure you must pass only UIViewController to this func.
I search viewController that already in stack by using .first this make me got only one VC then stop the loop.
I also return passing VC back if you need to customize somethings.
Let's enjoy.
extension UINavigationController {
func popToViewController<T: UIViewController>(_ viewController: T.Type, animated: Bool) -> T? {
guard let viewController = self.viewControllers.first(where: {$0 is T}) else { return nil }
self.popToViewController(viewController, animated: animated)
return viewController as? T
}
}
Usage
let poppedVC = self.navigationController?.popToViewController(HomeViewController.self, animated: true)
extension UINavigationController {
func popBack(to vc: AnyClass, animated: Bool = true) {
guard let elementFound = (viewControllers.filter { $0.isKind(of: vc) }).first else {
fatalError("cannot pop back to \(vc) as it is not in the view hierarchy")
}
self.popToViewController(elementFound, animated: animated)
}
}
simple and best solution without force unwrapped is
if let vc = navigationController.viewControllers.filter({$0 is YourViewController}).first as? YourViewController {
self.navigationController.popToViewController(vc, animated: true)
}

Resources