iOS Xcode (swift) - how to execute code after unwind segue - ios

I perform a segue from scene 1 to scene 2. I then return from scene 2 to scene 1. How do I not only pass data from scene 2 to scene 1 but detect in scene 1 that I've returned from scene 2 and execute code in scene 1?
In Android I do this with startActivity and onActivityResult.

Introducing Bool state like the other answer's suggesting is very bad and must be avoided if possible as it greatly increases the complexity of your app.
Amongst many other patterns, easiest one to solve this kind of problem is by passing delegate object to Controller2.
protocol Controller2Delegate {
func controller2DidReturn()
}
class Controller1: Controller2Delegate {
func controller2DidReturn() {
// your code.
}
func prepareForSegue(...) {
// get controller2 instance
controller2.delegate = self
}
}
class Controller2 {
var delegate: Controller2Delegate!
func done() {
// dismiss viewcontroller
delegate.controller2DidReturn()
}
}
States are evil and is the single biggest source of software bugs.

you could do it like this:
class SourceViewController: UIViewController {
var didReturnFromDestinationViewController = false
#IBAction func returnToSourceViewController(segue: UIStoryboardSegue) {
didReturnFromDestinationViewController = true
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if didReturnFromDestinationViewController == true {
// reset the value
didReturnFromDestinationViewController = false
// do whatever you want
}
}
}

The problem I was having was that I was trying to show an alert dialog after the unwind segue had finished. So my View Controller 2 performed an unwind segue to View Controller 1. What I found is that the code that runs after the unwind segue method is called runs before View Controller 2 is dismissed, so when I tried to show an alert dialog, it would disappear as soon as View Controller 2 was dismissed.
If the other solutions don't work for you, do what I did. I added a viewWillAppear override to my class and dismissed the parent controller there, then added the code for my alert dialog after that. To make sure viewWillAppear wasn't showing the alert dialog the first time View Controller 1 was presented, I set up an if statement checking for the name of a variable that I declared in the class and had set equal to "". Then in View Controller 2 I passed some text in the variable back to View Controller 1, so when the if statement runs it tests the variable not equal to "", and when it finds it's not, the code is run. In my case the variable was named "firstname".
override func viewWillAppear(_ animated: Bool) {
if firstname != "" {
self.parent?.dismiss(animated: true, completion: nil)
//CustomViewController?.dismiss(animated: true, completion: nil)
let alertController = UIAlertController(title: "Hello", message: "This is a test", preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "Close Alert", style: .default, handler: nil)
alertController.addAction(defaultAction)
present(alertController, animated: true, completion: nil)
}
}

Related

Correct way of presenting a ViewController B from ViewController A before A is visible

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
})
}
}

How to reload tableview after submission of a page of type present modally in swift

I have a post a review system in my app. a user clicks on the review button from the reviewTableViewController and a present modally page pops up that is a postReviewController and he can post a review. after the user clicks the send button, I made it that the present modally page dismisses, but now the post that the user has just written is not shown until I exit the reviewTableViewController and open it again.
how can I reload the table view after dismissing the present modally page?
i tried the unwind method on the click of my reviewBtn, but that cancels all the code that i need it to be executed.
my code:
#IBAction func ReviewBtn(_ sender: Any) {
db.collection("Reviews").addDocument(data:[
//my code here
]){ err in
if let err = err {
print("Error writing document: \(err)")
} else {
let messageVC = UIAlertController(title:
"Review submitted successfully!", message: "" , preferredStyle: .actionSheet)
self.present(messageVC, animated: true) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
messageVC.dismiss(animated: true) {
self.dismiss(animated: true, completion: nil)
}
}
}
}
}
}
any help or suggestions on this?
You can create a protocol and an object of the protocol in your presented controller as follows
protocol DismissView{
func updateTblData()
}
var delegate: DismissView?
When you dismiss the view call the method of the delegate as follows:
if let delegate = delegate {
delegate.updateTblData()
}
Now in your controller from where you presented the screen, please conform to the protocol and provide definition of the method.
Also when presenting the controller please assign the delegate to self as below:
secondController.delegate = self
The screen from which you present could look something like below:
class FirstViewController: UIViewController , DismissView {
#IBOutlet weak var tbl: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func click(_ sender: Any) {
let controller = storyboard?.instantiateViewController(identifier: "new") as! NewViewController
controller.delegate = self
present(controller, animated: true)
}
func test() {
tbl.updateTblData()
}
}
There are basically 2 ways in which you could do this.
Using a delegate: The idea is that your PostReviewController should call a method in your ReviewTableViewController instead of just dismissing itself. And in that case your ReviewTableViewController instance would dismiss the PostReviewController controller and call reloadData in the completion of the dismiss call. (just search for delegate in swift and you should find a lot of solution which will help you figure it out.
Using closures: basically you can pass a closure from ReviewTableViewController to your PostReviewController. In that closure you will call dismiss and reloadData in completion and your PostReviewController instance will call that closure instead of dismissing itself.
P.S. check the documentation of dismiss(animated:completion:). Ideally the presenting view controller should be responsible for dismissing the view controller it presented

How to present action sheet form a View Controller that present modally?

I have a ViewControllerA that already show as pop out,which is present modally. Inside this ViewControllerA have a tableview with tableViewCell .So in each cell have a button.When users click on this button,I want to show actionSheet at the bottom of the screen.
Here is my code:
TableViewCell class,here I connect the button to the IBAction
protocol MyDelegate: class {
func showDropDownMenu()
}
class tableCell: UITableViewCell {
weak var delegate: MyDelegate?
#IBAction func dropDownButton(_ sender: Any) {
print("print something")
self.delegate?.showDropDownMenu()
}
}
ViewControllerA
class ViewControllerA: UIViewController , UITableViewDataSource, UITableViewDelegate,MyDelegate {
func showDropDownMenu() {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
// Create your actions - take a look at different style attributes
let hideAction = UIAlertAction(title: "Hide", style: .default) { (action) in
print("didPress hide")
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action) in
print("didPress cancel")
}
actionSheet.addAction(hideAction)
actionSheet.addAction(cancelAction)
self.present(actionSheet, animated: true, completion: nil)
}
Suppose when click on the button will call the function showDropDownMenu() in ViewControllerA.Now I click on dropDownButton it will show print something in console(means no problem with the button),but the actionSheet not show up on the bottom.
I not sure what is the problem here,but I suspect is cause by ViewControllerA is present using segue with properties like so:
Kind: Present modally ,Presentation: Over Current Context ,Transition:
Cover Vertical
If this is the reason,please help me how to present an actionsheet from a View Controller that presented modally. Thank you
Code for showing ViewControllerA :
func showViewControllerA(Id: Int) {
self.performSegue(withIdentifier: "showViewControllerA", sender: Id)
}
Refer this link. Though it's for IPad, it will give you a brief idea of where and how to present the action sheet
https://medium.com/#nickmeehan/actionsheet-popover-on-ipad-in-swift-5768dfa82094
I have faced a similar scenario of what you are trying to achive, and this solution which i provided above helped me out. Hope, it helps you out as Well.
Make sure you have set value for delegate. For example
cell.delegate = self;

Showing alert after unwind segue stops the segue. How do I make sure alert is shown after unwind segue is completed?

I have an unwind segue from A view controller to B view controller.
A network operation is done in B. After the operation is completed, the response will be shown in A view controller.
I successfully made this structure. However there is an issue:
When I try to show the alert, it shows but stops the segue. How do i make sure alert shows after segue is completed.
The error is here:
2016-04-27 14:39:28.350 PROJECT[9100:128844] Presenting view controllers on detached view controllers is discouraged <PROJECT.FeedTableViewController: 0x7a928c00>.
2016-04-27 14:39:28.359 PROJECT[9100:128844] popToViewController:transition: called on <UINavigationController 0x7c12a800> while an existing transition or presentation is occurring; the navigation stack will not be updated.
Unwind handler in A:
#IBAction func unwindToFeed(segue: UIStoryboardSegue) {
jsonArray[rowFromShare!]["ApplicationDataUsers"] = jsonFromShare!
tableView.reloadData()
ShowErrorDialog("Success", message: successMessageFromShare!, buttonTitle: "OK")
}
//Error Dialog
func ShowErrorDialog(title:String, message:String, buttonTitle:String){
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default) { _ in })
self.presentViewController(alert, animated: true){}
}
Unwind trigger in B:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "unwindToFeed"{
let feedTable = segue.destinationViewController as! FeedTableViewController
feedTable.rowFromShare = row
feedTable.jsonFromShare = jsonToShare
feedTable.successMessageFromShare = successMessageToShare
}
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
A = FeedTableViewController
B = ShareTableViewController
How do I make sure alert is shown after segue is done?
The unwindToFeed method is called before the unwind segue is complete, as you have found.
One approach would be to set a boolean in the unwindToFeed method and then check this boolean in viewDidAppear, when you know the segue is complete. If the boolean is set then you can display the alert:
#IBAction func unwindToFeed(segue: UIStoryboardSegue) {
jsonArray[rowFromShare!]["ApplicationDataUsers"] = jsonFromShare!
tableView.reloadData()
self.unwinding = true
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if (self.unwinding) {
self.ShowErrorDialog("Success", message: successMessageFromShare!, buttonTitle: "OK")
self.unwinding=false
}

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!

Resources