I have a mainViewController containing a tableView and each cell pushes a specific customViewController. When it gets popped I want to trigger different function. So my question is how to I identify the viewController that got popped after it got popped.
The first log reads
lastObject = Optional(<Project.ViewController: 0x61900009c480>) but in general the idea does not work. Cannot find anything similar on the web. Any ideas? Thanks in advance
override func viewDidAppear(_ animated: Bool) {
print("lastObject = \(String(describing: (self.navigationController?.viewControllers.last)))")
if ((navigationController?.viewControllers.last?.isKind(of:WebViewController.self)) == true){
print("last is web")
}else{
print("is not web")
}
Only way is you could subclass the UINavigationController and override - popViewControllerAnimated. I haven't tried this yet, but possibly a solution.
Maybe not a best solution but for an temp solution:
If you have have very less cell in the UITableView then you could save the ViewController name in UserDefaults when the viewController is popped and validate the name in the viewDidAppear of mainViewController
override func viewDidAppear(_ animated: Bool) {
if (UserDefaultsValue == "FirsViewController"){
print("last is FirsViewController")
} else if(UserDefaultsValue == "SecondViewController") {
print("last is SecondViewController")
}
}
Create one protocol for all controllers something like this
protocol WhichPoppedDelegate: class {
func controllerDidPopped(controller: UIViewController)
}
assign delegates for each in mainContorller and just call that protocol before you call
navigationController?.popViewController(animated: true)
Related
My iOS App starts with UIViewController A which is embedded as first element in a UINavigationController. When the app is started or when returning to it after some time in background I would like to show a password prompt. In this case UIViewController A should present UIViewController B which shows the password prompt.
The user should immediately see UIViewController B, not A and then B sliding in, etc. Thus, I have presented UIViewController B in viewWillAppear in UIViewController A:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: nil)
}
}
This works fine, but an error message is logged:
Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x7fe9af01c200>.
It is obvious that presenting UIViewController B from UIViewController A before it became visible causes this problem. Moving from viewWillAppear to viewDidAppear would solve the error message. However, than the user would first see A then B...
Is it even possible to overlay a ViewControler A with ViewController B without A becoming visible first?
I know that there might be other solutions like adding the view of the password ViewController manually to the view hierachy, etc. However, I would prefer a clean way where A is in complete control. Is this possible?
Or is it save to simple ignore the warning?
It sounds a bit tricky, might do the job though.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
self.view.alpha = 0
// Maybe (or not?)
self.navigationController?.view.backgroundColor = .white
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: { [weak self] in
self?.view.alpha = 1
})
}
}
I know this is a pretty common question but I've tried the various solutions offered here (that are not too old) and in numerous tutorials and I just can't seem to find out why it's still failing for me. Basically setting sendingViewController.delegate to self ends up being nil in sendingViewController. I understand this is very likely because the reference to the sendingViewController is being disposed of. But here is why I'm asking this again.
First, almost every tutorial and every other StackOverflow post is wiring up the mainViewController and the sendingViewController differently. I'm trying to make this work through a Navigation Controller, what one would think is the most common pattern for this.
In the app I'm building (which is more complex than the sample I'm going to show), the mainViewController calls the Settings viewController through a right navbar button. Then the user can select items from a list, which opens a controller with a searchBar and a tableView of items to select from. I need that third view controller to return the selected item from the table view to the settings screen. I'm using storyboards as well. I'm fairly new to Swift and I'm not ready to do all this "programmatically". Any way in the sending view controller, my delegate which should have been set in the calling view controller is nil and I can't invoke the protocol function in the main view controller to pass the data back.
I did a tutorial directly (not using Nav controllers) and I got that to work, but the moment I deviate away, it starts failing. I then put together a streamlined project with two view controllers: ViewController and SendingViewController. ViewController was embedded in a navigation controller and a right bar button was added to go to the SendingViewController. The SendingViewController has a single UI Button that attempts to call the protocol function and dismiss the SendingViewController. I'm not using Seque's, just a simple buttons and protocol/delegate pattern as I can.
My question is what am I missing to actually set the SendingViewController.delegate correctly?
Here's some code:
//ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var showDataLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func fetchDataButton(_ sender: UIBarButtonItem) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "SendingViewController") as! SendingViewController
controller.delegate = self
print("fetching data")
present(controller, animated: true, completion: nil)
}
}
extension ViewController: SendingViewControllerDelegate {
func sendData(value: String) {
print("got Data \(value)")
self.showDataLabel.text = value
}
}
and
// SendingViewController.swift
import UIKit
protocol SendingViewControllerDelegate {
func sendData(value: String)
}
class SendingViewController: UIViewController {
var delegate: SendingViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func sendDataButton(_ sender: UIButton) {
print("attempting to send data \(self)")
print("to \(self.delegate)")
self.delegate?.sendData(value: "Hello World")
self.navigationController?.popViewController(animated: true)
}
}
Here is a screenshot of the Storyboard:
The ChildViewController does have a storyboard id name of "ChildViewController". All buttons and labels have their appropriate IBOutlet and IBAction's set up.
Help!
i copy paste your code .. its working perfect .. i make just one change
instead of pop you need to use dismiss as you are presenting from your base viewController
#IBAction func sendDataButton(_ sender: UIButton) {
print("attempting to send data \(self)")
print("to \(self.delegate)")
self.delegate?.sendData(value: "Hello World")
self.dismiss(animated: true, completion: nil)
}
here is the project link we.tl/t-NUxm9D26XN
I managed to get this working. In the receiving/parent view controller that needs the data:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let controller = segue.destination as! sendingViewController
controller.cityDelegate = self
}
Then in the sending view controller in my tableView did select row function:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let city = filtered[indexPath.row]
searchBar.resignFirstResponder()
self.navigationController?.popViewController(animated: true)
self.cityDelegate?.addCity(city)
self.dismiss(animated: true, completion: nil)
}
I don't think I should be both popping the view controller and dismissing it, but it works. Also in the view controller I did this:
private var presentingController: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presentingController = presentingViewController
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if parent == nil {
}
}
I don't know if I really need this didMove() or not since it doesn't really do anything.
But some combination of all this got it working.
In my other app I'm not using a navigation bar controller and the standard delegate/protocol method works like a charm.
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
So I have 3 view controllers: TableViewController, A, and B. The user is able to navigate to any view controller from any view controller.
When the user goes back and forth between A, and B view controllers I want them to be pushed onto the nav. stack. When the "home" button is pressed, I would like for the view controllers to all be popped back to the TableViewController using popToViewController, not popToRootViewController (for reasons).
I have partly working code that pops the last visited view controller, but now all the ones in between.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row == 0 {
if let navController = self.navigationController {
for controller in navController.viewControllers {
if controller is TableViewController {
navController.popToViewController(controller, animated: true)
break
}
}
}
} else {
let vcName = identities[indexPath.row]
let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
self.navigationController?.pushViewController(viewController!, animated: true)
}
}
I'm not sure why all the view controllers aren't being popped.
Code I use to check what's being pushed and popped:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if self.isMovingToParentViewController {
print("A is pushed")
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
if self.isMovingFromParentViewController {
print("A is popped")
}
}
I'm also checking increase in memory.
I will provide more code/info in needed.
Any help would be greatly appreciated.
Your confusion may simply be the way you are trying to "check" that the VCs are "popped".
Suppose you have gone:
root->TableView->A->B->A->B->B->B->`
At that point, the only VC that is visible is the last instance of A. So when you call
navController.popToViewController(controller, animated: true)
viewWillDisappear() will only be called on the last instance of A - none of the other VC instances will "disappear" because they are not visible.
If you want to confirm the other VCs in the stack are being "removed", put this in each view controller:
deinit() {
print("I'm being removed:", self)
}
The other part of the question - do you want to animate through the process? So you would actually see the VCs "walk back up the stack"? If so, follow #FryAnEgg's link to Completion block for popViewController
Try something like this:
var theControllerIWantToPopTo = controllerB // or whatever other condition
if let navController = self.navigationController {
for controller in navController.viewControllers {
if controller is TableViewController {
if controller == theControllerIWantToPopTo {
navController.popToViewController(controller, animated: true)
break
}
}
}
}
Remember, popToViewController will pop all controllers until the chosen one is on top, as opposed to popViewController which will only pop the top controller. If you want to pop them one at a time with animation on each pop see: Completion block for popViewController
In my app I have three table view controllers and then potentially many UIViewControllers each of which has to lead back to the first table view controller if the user presses back at any point. I don't want the user to have to back through potentially hundreds of pages. This is what I amusing to determine if the user pressed the back button and it works the message is printed
override func viewWillDisappear(_ animated: Bool) {
if !movingForward {
print("moving back")
let startvc = self.storyboard!.instantiateViewController(withIdentifier: "FirstTableViewController")
_ = self.navigationController!.popToViewController(startvc, animated: true)
}
}
I have searched and none of the solutions have worked so far.
popToViewController not work in a way you are trying you are passing a complete new reference of FirstTableViewController instead of the one that is in the navigation stack. So you need to loop through the navigationController?.viewControllers and find the FirstTableViewController and then call popToViewController with that instance of FirstTableViewController.
for vc in (self.navigationController?.viewControllers ?? []) {
if vc is FirstTableViewController {
_ = self.navigationController?.popToViewController(vc, animated: true)
break
}
}
If you want to move to First Screen then you probably looking for popToRootViewController instead of popToViewController.
_ = self.navigationController?.popToRootViewController(animated: true)
Try this :
let allViewController: [UIViewController] = self.navigationController!.viewControllers as [UIViewController];
for aviewcontroller : UIViewController in allViewController
{
if aviewcontroller .isKindOfClass(YourDestinationViewControllerName)// change with your class
{
self.navigationController?.popToViewController(aviewcontroller, animated: true)
}
}
If you are in a callback, particularly an async network callback, you may not be on the main thread. If that's you're problem, the solution is:
DispatchQueue.main.async {
self.navigationController?.popToViewController(startvc, animated: true)
}
The system call viewWillDisappear() is always called on the main thread.