Swift tableView.reloadData() not working inside completion handler - ios

I have two view controllers, in controller 1 I have a table view and I am presenting the controller 2 like so:
#IBAction func addPOItemButtonPressed(_ sender: Any) {
let itemViewController = self.storyboard?.instantiateViewController(withIdentifier: "LPOsItemController") as! LPOsItemController
itemViewController.RequestID = self.RequestID
let navigationViewController = UINavigationController(rootViewController: itemViewController)
self.present(navigationViewController, animated: true, completion: nil)
}
When I dismiss controller 2 I am calling a method in controller 1 like so:
In Controller 1:
NotificationCenter.default.addObserver(self, selector: #selector(self.refreshDetails(notification:)), name:NSNotification.Name(rawValue: "refreshDetails"), object: nil)
#objc func refreshDetails(notification: NSNotification){
spLPOHeaderGet() { result in
self.tableView.reloadData()
}
}
In Controller 2
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshDetails"), object: nil)
dismiss(animated: true, completion: nil)
My problem is when I call the method in Controller 1, the table view does not reload the data, sometimes it does and sometimes it doesn't. The reload call is in a completion handler. Can someone tell me what I am doing wrong?
Thanks,

Posting to notification for this situation is not a good idea. You can use the delegate to communicate from ViewController 2 to controller 1.
if you want to identify an issue in the current implementation try to put a breakpoint on
self.tableView.reloadData()
and check this method is called. and if it is called does tableview is in memory when this called. There is a chance when the completion handler is called this tableView object is deallocted.

Related

NotificationCenter observer clearing after presenting modal

My app is playing a video and I want to trigger an action when the video ends. The screen (A) is embedded in a navigation controller, and if I trigger a push (to B) and then come back (to A), the action (in A) still takes place based on the observer. There is also an option in my screen that triggers a modal (to C) which then gets dismissed to go back (to A). When I come back from the modal (C), however, the observer (in A) is gone.
Here's my code for screen A's view controller:
ScreenAViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do more stuff
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.avPlayer.currentItem, queue: .main) { _ in
// Do stuff
}
}
}
Here's the code that trigger the modal to screen C:
#IBAction func triggerModal(_ sender: UIButton) {
avPlayer.pause()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let screenCViewController = storyboard.instantiateViewController(withIdentifier: "ScreenC") as! ScreenCViewController
present(screenCViewController, animated: true)
}
And finally here's the line that dismisses screen C:
dismiss(animated: true, completion: nil)
addObserver returns an observer object. You are ignoring this and not retaining it. Therefore it goes out of scope and dies and the observation ends.

Swift 3: popToViewController not working

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.

Warning: Attempt to present * on * which is already presenting (null)

This is my first application for iOS.
So I have a UIVIewController with a UITableView where I have integrated a UISearchBar and a UISearchController in order to filter TableCells to display
override func viewDidLoad() {
menuBar.delegate = self
table.dataSource = self
table.delegate = self
let nib = UINib(nibName: "ItemCellTableViewCell", bundle: nil)
table.registerNib(nib, forCellReuseIdentifier: "Cell")
let searchButton = UIBarButtonItem(barButtonSystemItem: .Search, target: self, action: "search:")
menuBar.topItem?.leftBarButtonItem = searchButton
self.resultSearchController = ({
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
return controller
})()
self.table.reloadData()
}
I am using also a modal segue in order to open the element's ViewController where I will display details of the element.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.index = indexPath.row
self.performSegueWithIdentifier("ItemDetailFromHome", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "ItemDetailFromHome") {
let settingsVC = segue.destinationViewController as! ItemDetailViewController
settingsVC.parent = self
if self.isSearching == true && self.searchText != nil && self.searchText != "" {
settingsVC.item = self.filteredItems[self.index!]
} else {
settingsVC.item = self.items[self.index!]
}
}
}
That works fine until I try to display the ItemDetailViewController for a filtered element (through the UISearchController).
I have the following message :
Warning: Attempt to present <ItemDetailViewController: *> on <HomeViewController: *> which is already presenting (null)
At every time I am going to the ItemDetailViewController.viewDidLoad() function but after that when the search is activated I have the previous error.
Any idea ? I have tried to use the following async dispatch but without success
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.index = indexPath.row
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("ItemDetailFromHome", sender: self)
})
}
I have found out a solution.
I have add the following code in HomeViewController.viewDidLoad and that works !
definesPresentationContext = true
In my case, I found my code to present the new viewController (a UIAlertController) was being called twice.
Check this before messing about with definesPresentationContext.
In my case, I tried too early to show the new UIViewController before closing the previous one. The problem was solved through a call with a slight delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.callMethod()
}
The problem for me is that I was presenting two modals and I have to dismiss both and then execute some code in the parent window ... and then I have this error... I solved it seting this code in the dismiss o the last modal presented:
self.dismiss(animated: true, completion: {
self.delegate?.callingDelegate()
})
in other words instead of just dismiss two times .. in the completion block of the first dismiss call delegate that will execute the second dismiss.
What worked for me was to add the presentation of the alert to the main thread.
DispatchQueue.main.async {
self.present(alert, animated: true)
}
The presentation of the current viewController was not complete. By adding the alert to the main thread it can wait for the viewController's presentation to complete before attempting to present.
I got the same issue when i tried to present a VC which called inside the SideMenu(jonkykong).
first i tried inside the SideMenu and i called it from the delegate to the MainVC both had the same issue.
Solution: dismiss the SideMenu first and present the new VC after will works perfectly!.
This happened with me on our project. I was presenting our log in/log out ViewController as a pop-over. But whenever I tried to log back out again and display the pop-over again, I was getting this logged out in my console:
Warning: Attempt to present UIViewController on <MY_HOME_VIEW_CONTROLLER> which is already presenting (null)
My guess is that the pop-over was still being held by my ViewController even though it was not visible.
However you are attempting to display the new ViewController, the following code I used to solve the issue should work for you:
func showLoginForm() {
// Dismiss the Old
if let presented = self.presentedViewController {
presented.removeFromParentViewController()
}
// Present the New
let storyboard = UIStoryboard(name: "MPTLogin", bundle: Bundle(for: MPTLogin.self))
let loginVC = storyboard.instantiateViewController(withIdentifier: "LogInViewController") as? MPTLogInViewController
let loginNav = MPTLoginNav(rootViewController: loginVC!)
loginNav.modalPresentationStyle = .pageSheet;
self.present(loginNav, animated: true, completion: nil)
}
I faced the same kind of problem
What I did is from Interface builder selected my segue
Its kind was "Present Modally"
and its presentation was "Over current context"
i changed the presentation to "Default", and then it worked for me.
In my case I was trying to present a UIAlertController at some point in the app's lifetime after using a UISearchController in the same UINavigationController.
I wasn't using the UISearchController correctly and forgot to set searchController.isActive = false before dismissing. Later on in the app I tried to present the alert but the search controller, though not visible at the time, was still controlling the presentation context.
My problem was that (in my coordinator) i had presented a VC on a VC and then when i wanted to present the next VC(third one), presented the third VC from the first one which obviously makes the problem which is already presenting.
make sure you are presenting the third one from the second VC.
secondVC.present(thirdVC, animated: true, completion: nil)
Building on Mehrdad's answer: I had to first check if the search controller is active (if the user is currently searching):
if self.searchController.isActive {
self.searchController.present(alert, animated: true, completion: nil)
} else {
self.present(alert, animated: true, completion: nil)
}
where alert is the view controller to present modally.
This is what finally worked for me, as my project didn't exactly have a NavigationVC but instead, individual detached VC's. as xib files
This code produced the bug:
present(alertVC, animated: true, completion: nil)
This code fixed the bug:
if presentedViewController == nil{
navigationController?.present(alertVC, animated: true, completion: nil)
}
For me it was an alert that was interfering with the new VC that I was about to present.
So I moved the new VC present code into the OK part of my alert, Like this :
func showSuccessfullSignupAndGoToMainView(){
let alert = UIAlertController(title: "Alert", message: "Sign up was successfull.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
switch action.style{
case .default:
// Goto Main Page to show businesses
let mainStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc : MainViewController = mainStoryboard.instantiateViewController(withIdentifier: "MainViewController") as! MainViewController
self.present(vc, animated: false, completion: nil)
case .cancel:
print("cancel")
case .destructive:
print("destructive")
}}))
self.present(alert, animated: true, completion: nil)
}
My issue was that I was trying to present an alert from a view that wasn't on top. Make sure you present from the top-most viewController.
In my case this was an issue of a button which was duplicated in Interface Builder. The original button had a touch-up handler attached, which also presented a modal view. When I then attached a touch-up handler on the copied button, I forgot to remove the copied handler from the original, causing both handlers to be fired and thus creating the warning.
More than likely you have your Search button wired directly to the other view controller with a segue and you are calling performSegueWithIdentifier. So you are opening it twice, which generates the error that tells you "is already presenting."
So don't call performSegueWithIdentifier, and that should do the trick.
Make sure you Dismiss previous one before presenting new one!

IOS Swift handle global events

How can I handle global events triggered by the notification centre for example in my API class I fire an event if an error response is received e.g. (500). When that event is fired an UIAlert should be displayed on what ever view controller is active, or on logout the login view controller should be presented.
As far as I can see there is no easy way to get the current view controller in order to interact with it. (Note that my root view controller is NOT a navigation controller).
An alternative solution, that will work regardless of whether your view controllers are embedded in a UINavigationController or not, would be to subclass UIViewController. This class will handle receiving the NSNotification that an error occurred and will also handle displaying the alert:
class MyViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "errorOccured",
name: "ErrorNotification",
object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "ErrorNotification", object: nil)
}
func errorOccured() {
// Present an UIAlertViewController or the login screen.
}
}
Now, any UIViewControllers that should display an alert when the error notification is posted just have to be a subclass of MyViewController. Just make sure, if you override viewWillAppear or viewWillDisappear, that you call super.viewWillAppear or super.viewWillDisappear.
Is this way too hard to get current view controller ( when not using navigation controller ) ?
// on your app delegate
getCurrentViewController(self.window!.rootViewController!)
func getCurrentViewController(viewController:UIViewController)-> UIViewController{
if let navigationController = viewController as? UINavigationController{
return getCurrentViewController(navigationController.visibleViewController)
}
if let viewController = viewController?.presentedViewController {
return getCurrentViewController(viewController)
}else{
return viewController
}
}
For BroadCast Notification
NSNotificationCenter.defaultCenter().postNotificationName("erro400", object: nil)
For Receive
override func viewWillAppear(animated: Bool) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "ErroOccure", name: "erro400", object: nil)
}
func ErroOccure()
{
//present alert from here
// do whatever you want
}
You have to Remove Notification when you finish with it.
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self)
}

How to reload tableview from another view controller in swift

When I dismiss a modal view controller I want the tableview to update, I am using the form sheet presentation style on iPad so the viewWillAppear and the viewDidAppear methods will not work
You can do this:
In your tableView Controller:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(loadList), name: NSNotification.Name(rawValue: "load"), object: nil)
}
#objc func loadList(notification: NSNotification){
//load data here
self.tableView.reloadData()
}
Then in the other ViewController :
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "load"), object: nil)
Swift 3 version code:
In your first view controller:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(loadList), name: NSNotification.Name(rawValue: "load"), object: nil)
}
func loadList(){
//load data here
self.tableView.reloadData()
}
In your second view controller:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "load"), object: nil)
I find the segue approach more elegant.
Let's say that we have ViewControllerA and a ViewControllerB. We are at ViewControllerB and we want from ViewControllerB to jump straight back to ViewControllerA and update the table view in ViewControllerA.
In ViewControllerA add the following action in your ViewController class:
#IBAction func unwindToViewControllerA(segue: UIStoryboardSegue) {
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Yes, this code goes in the ViewController of the ViewController you want to go back to!
Now, you need to create an exit segue from the ViewControllerB's storyboard (StoryboardB). Go ahead and open StoryboardB and select the storyboard. Hold CTRL down and drag to exit as follows:
You will be given a list of segues to choose from including the one we just created:
You should now have a segue, click on it:
Go in the inspector and set a unique id:
In the ViewControllerB at the point where you want to dismiss and return back to ViewControllerA, do the following (with the id we set in the inspector previously):
self.performSegue(withIdentifier: "yourIdHere", sender: self)
Now, whenever you use the segue to return back to ViewControllerA, the ViewControllerA will update the TableView straightaway.
Missing comma on this line, should instead be:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "loadList:", name:"load", object: nil)
An alternate solution: override UIViewController's dismiss(animated:completion:) method on the view controller that manages the table view (and presents the other one modally), so you can reload the table then:
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
super.dismiss(animated: flag, completion: completion)
self.tableView.reloadData()
}
Note: This should work even if you call dismiss(animated:completion:) on the modal view controller itself (a viable alternative), because ultimately the call gets relayed to the presenting view controller.
From the method's docs:
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
(emphasis mine)
You can use NotificationCenter to update your tableview.
First add observer...
NotificationCenter.default.addObserver(self, selector: #selector(doThisWhenNotify(notification:)), name: NSNotification.Name(rawValue: "load"), object: nil)
func doThisWhenNotify(notification : NSNotification) {
let info = notificatio.userInfo
//update tableview
}
Post on other ViewController
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "load"), object: nil, userInfo: [String : Any])

Resources